MidПрактика3 min

Интеграция фронтенда и бэкенда

Подключение HTML-резюме к API через JavaScript fetch, обработка ошибок и smoke-тестирование

Архитектура интеграции

Браузер пользователя
    │
    ├── GET index.html ──→ CloudFront ──→ S3 (HTML/CSS/JS)
    │
    └── GET /count ──→ API Gateway ──→ Lambda ──→ DynamoDB
                          │
                          └── Response: {"count": 42}

Фронтенд (HTML/CSS/JS) и бэкенд (API) -- это два отдельных компонента, связанных через HTTP-запросы.

JavaScript Fetch API

Базовая реализация счётчика

// counter.js
const API_URL = 'https://api.ivanpetrov.com/count';

async function updateVisitorCount() {
  const counterElement = document.getElementById('visitor-count');

  try {
    const response = await fetch(API_URL);

    if (!response.ok) {
      throw new Error(`HTTP error: ${response.status}`);
    }

    const data = await response.json();
    counterElement.textContent = data.count;

  } catch (error) {
    console.error('Failed to fetch visitor count:', error);
    counterElement.textContent = 'unavailable';
  }
}

// Call when page loads
document.addEventListener('DOMContentLoaded', updateVisitorCount);

Улучшенная версия с retry

// counter.js - Production-ready version
const API_URL = 'https://api.ivanpetrov.com/count';
const MAX_RETRIES = 3;
const RETRY_DELAY = 1000; // ms

async function fetchWithRetry(url, retries = MAX_RETRIES) {
  for (let attempt = 1; attempt <= retries; attempt++) {
    try {
      const controller = new AbortController();
      const timeoutId = setTimeout(() => controller.abort(), 5000);

      const response = await fetch(url, { signal: controller.signal });
      clearTimeout(timeoutId);

      if (!response.ok) {
        throw new Error(`HTTP ${response.status}`);
      }

      return await response.json();
    } catch (error) {
      if (attempt === retries) throw error;
      await new Promise(r => setTimeout(r, RETRY_DELAY * attempt));
    }
  }
}

async function updateVisitorCount() {
  const el = document.getElementById('visitor-count');

  try {
    const data = await fetchWithRetry(API_URL);
    el.textContent = new Intl.NumberFormat('ru-RU').format(data.count);
    el.classList.add('loaded');
  } catch (error) {
    console.error('Visitor counter error:', error);
    el.textContent = '—';
    el.title = 'Не удалось загрузить счётчик';
  }
}

document.addEventListener('DOMContentLoaded', updateVisitorCount);

Подключение к HTML

<!-- В footer секции index.html -->
<footer>
  <p>
    Посетителей:
    <span id="visitor-count" aria-live="polite">загрузка...</span>
  </p>
</footer>

<!-- Перед закрывающим </body> -->
<script src="counter.js"></script>

Обработка ошибок

Типичные ошибки и решения

Ошибка Причина Решение
TypeError: Failed to fetch CORS блокирует запрос Настроить CORS на API Gateway
403 Forbidden Нет доступа к API Проверить IAM permissions
500 Internal Server Error Ошибка в Lambda Проверить CloudWatch Logs
net::ERR_CONNECTION_TIMED_OUT API недоступен Проверить API Gateway stage

Debugging в браузере

// Откройте DevTools (F12) → Console → Network

// 1. Проверьте, что запрос уходит
// Network tab: должен быть запрос к вашему API URL

// 2. Проверьте CORS
// Console: если видите "CORS policy" ошибку:
// "Access to fetch at 'https://api...' from origin 'https://resume...'
//  has been blocked by CORS policy"

// 3. Проверьте ответ
// Network tab → click request → Response tab
// Должен быть JSON: {"count": 42}

// 4. Проверьте заголовки ответа
// Network tab → click request → Headers tab
// Должен быть: access-control-allow-origin: https://resume.ivanpetrov.com

Smoke Testing

Что такое smoke test

Smoke test -- это минимальная проверка, что всё работает. Для CRC:

  1. Фронтенд загружается
  2. API отвечает
  3. Счётчик обновляется
  4. HTTPS работает

Автоматический smoke test

#!/bin/bash
# smoke-test.sh

FRONTEND_URL="https://resume.ivanpetrov.com"
API_URL="https://api.ivanpetrov.com/count"

echo "=== CRC Smoke Test ==="

# Test 1: Frontend loads
echo -n "Frontend... "
STATUS=$(curl -s -o /dev/null -w "%{http_code}" "$FRONTEND_URL")
if [ "$STATUS" -eq 200 ]; then
  echo "OK (HTTP $STATUS)"
else
  echo "FAIL (HTTP $STATUS)"
  exit 1
fi

# Test 2: API responds
echo -n "API... "
RESPONSE=$(curl -s -w "\n%{http_code}" "$API_URL")
HTTP_CODE=$(echo "$RESPONSE" | tail -1)
BODY=$(echo "$RESPONSE" | head -1)

if [ "$HTTP_CODE" -eq 200 ]; then
  echo "OK (HTTP $HTTP_CODE)"
  echo "  Response: $BODY"
else
  echo "FAIL (HTTP $HTTP_CODE)"
  exit 1
fi

# Test 3: Response has count
echo -n "Count field... "
COUNT=$(echo "$BODY" | python3 -c "import sys,json; print(json.load(sys.stdin)['count'])" 2>/dev/null)
if [ -n "$COUNT" ]; then
  echo "OK (count=$COUNT)"
else
  echo "FAIL (no count in response)"
  exit 1
fi

# Test 4: HTTPS
echo -n "HTTPS... "
CERT=$(curl -s -o /dev/null -w "%{ssl_verify_result}" "$FRONTEND_URL")
if [ "$CERT" -eq 0 ]; then
  echo "OK (valid certificate)"
else
  echo "FAIL (certificate issue)"
  exit 1
fi

# Test 5: CORS headers
echo -n "CORS... "
CORS=$(curl -s -H "Origin: $FRONTEND_URL" -I "$API_URL" | grep -i "access-control-allow-origin")
if [ -n "$CORS" ]; then
  echo "OK"
else
  echo "WARN (no CORS headers visible)"
fi

echo "=== All checks passed ==="

Порядок деплоя

Правильный порядок

  1. Бэкенд первый -- деплоите Lambda, DynamoDB, API Gateway
  2. Тестируете API -- curl к эндпоинту, убедитесь что работает
  3. Фронтенд второй -- деплоите HTML/CSS/JS с правильным API URL
  4. Smoke test -- проверяете всё вместе

Частая ошибка: захардкоженный URL

// ПЛОХО: хардкод URL для разработки
const API_URL = 'http://localhost:3000/count';

// ХОРОШО: URL зависит от окружения
const API_URL = window.location.hostname === 'localhost'
  ? 'http://localhost:3000/count'
  : 'https://api.ivanpetrov.com/count';

Ещё лучше -- вынести URL в конфигурацию, которая подставляется при деплое.

Проверь себя

Зачем используется атрибут aria-live='polite' на элементе счётчика?

Что означает ошибка 'Access to fetch has been blocked by CORS policy' в консоли браузера?

Что такое smoke test в контексте CRC?

В каком порядке нужно деплоить компоненты CRC?

Почему рекомендуется добавить AbortController с таймаутом в fetch-запрос?