Урок 03

API Gateway

Что такое API Gateway

API Gateway — единая точка входа для клиентов.

Зачем:

  • Один домен для фронта.
  • Авторизация в одном месте.
  • Rate limiting общий.
  • Маршрутизация к сервисам.
  • Логирование запросов.

Пример:

api.example.com/* → Laravel
api.example.com/ws/* → Node.js
api.example.com/ai/* → Python

Зачем нужен Gateway, если есть Nginx

Nginx — балансировщик и reverse proxy. API Gateway — умная маршрутизация + бизнес-логика на входе.

Gateway умеет:

  • Проверять JWT.
  • Лимитировать по пользователю.
  • Трансформировать запросы.
  • Собирать метрики.

Nginx умеет:

  • Терминировать SSL.
  • Отдавать статику.
  • Балансировать.
  • Кэшировать.

Часто Nginx = Gateway. Если логики мало — хватит Nginx.

Nginx как Gateway

Маршрутизация по сервисам:

upstream laravel {
    server 127.0.0.1:9000;
}

upstream nodejs {
    server 127.0.0.1:3000;
}

server {
    listen 443 ssl http2;
    server_name api.example.com;

    # Laravel API
    location /api/ {
        fastcgi_pass laravel;
        include fastcgi_params;
    }

    # WebSocket
    location /ws/ {
        proxy_pass http://nodejs;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
    }

    # Node.js API
    location /realtime/ {
        proxy_pass http://nodejs;
    }
}

Что даёт: один домен, разные сервисы.

Маршрутизация по путям

Правило:

  • /api/* — Laravel (бизнес-логика).
  • /ws/* — Node.js (WebSocket).
  • /ai/* — Python (ML).
  • /files/* — статика.

В Laravel ничего не меняется — он обслуживает /api/*.

Node.js слушает /ws/* и /realtime/*.

Авторизация на Gateway

Вариант 1 — на Laravel:

Client → Gateway → Laravel → Sanctum → Service

Плюсы: одна логика. Минусы: Node.js не может проверить.

Вариант 2 — на Gateway (Nginx + Lua / Kong):

location /api/ {
    access_by_lua_block {
        local jwt = require "resty.jwt"
        local token = ngx.req.get_headers()["Authorization"]
        -- валидация JWT
    }
    proxy_pass http://laravel;
}

Плюсы: все сервисы защищены. Минусы: сложнее.

Вариант 3 — общий Redis:

Laravel сохраняет токен в Redis, Node.js читает:

const session = await redis.get(`session:${token}`);
if (!session) return res.status(401).json({ error: 'Unauthorized' });

Плюсы: просто. Минусы: доп. запрос в Redis.

Rate limiting на Gateway

Nginx:

limit_req_zone $binary_remote_addr zone=api:10m rate=100r/s;

location /api/ {
    limit_req zone=api burst=200 nodelay;
    proxy_pass http://laravel;
}

Что даёт: не более 100 запросов в секунду с одного IP.

Для WebSocket — отдельный лимит:

limit_conn_zone $binary_remote_addr zone=ws:10m;

location /ws/ {
    limit_conn ws 5;   # 5 соединений на IP
}

Отдача статики

Nginx отдаёт напрямую:

location /storage/ {
    alias /var/www/app/storage/app/public/;
    expires 30d;
    add_header Cache-Control "public, immutable";
}

location ~* \.(jpg|jpeg|png|gif|ico|css|js|woff2)$ {
    expires 1y;
    add_header Cache-Control "public, immutable";
}

Что даёт: не грузить PHP и Node статикой.

CORS на Gateway

Проблема: фронт с app.example.com, API на api.example.com.

Решение 1 — Nginx:

location /api/ {
    add_header 'Access-Control-Allow-Origin' 'https://app.example.com' always;
    add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always;
    add_header 'Access-Control-Allow-Headers' 'Authorization, Content-Type' always;
    add_header 'Access-Control-Allow-Credentials' 'true' always;

    if ($request_method = 'OPTIONS') {
        return 204;
    }

    fastcgi_pass laravel;
}

Решение 2 — Laravel middleware:

class Cors
{
    public function handle($request, Closure $next)
    {
        $response = $next($request);

        $response->headers->set('Access-Control-Allow-Origin', config('app.frontend_url'));
        $response->headers->set('Access-Control-Allow-Credentials', 'true');

        return $response;
    }
}

⚠️ Нельзя одновременно * и credentials: true.

Стриминг ответов

Проблема: AI-ответ генерируется 30 секунд.

Решение — SSE / стрим через Gateway:

location /ai/ {
    proxy_pass http://python;
    proxy_buffering off;              # не буферизировать
    proxy_read_timeout 300s;
    chunked_transfer_encoding on;
}

Что даёт: клиент видит ответ по кусочкам.

В Laravel:

return response()->stream(function () {
    foreach ($this->generateTokens() as $token) {
        echo "data: " . json_encode($token) . "\n\n";
        ob_flush();
        flush();
    }
}, 200, [
    'Content-Type' => 'text/event-stream',
    'Cache-Control' => 'no-cache',
    'X-Accel-Buffering' => 'no',   # отключить буфер Nginx
]);

Логирование на Gateway

log_format json_combined escape=json
    '{"time":"$time_iso8601",'
    '"method":"$request_method",'
    '"uri":"$request_uri",'
    '"status":$status,'
    '"duration":$request_time,'
    '"upstream":"$upstream_addr",'
    '"user_id":"$http_x_user_id"}';

access_log /var/log/nginx/api.log json_combined;

Что даёт: структурированные логи для Grafana / Loki.

Health-check

Каждый сервис:

location /health {
    access_log off;
    proxy_pass http://laravel/up;
    proxy_connect_timeout 1s;
    proxy_read_timeout 2s;
}

Что проверяет мониторинг:

curl -f https://api.example.com/health || alert

WebSocket через Gateway

Ключевые настройки:

location /ws/ {
    proxy_pass http://nodejs;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
    proxy_set_header Host $host;
    proxy_read_timeout 3600s;      # 1 час
    proxy_send_timeout 3600s;
}

⚠️ Без Upgrade и Connection WebSocket не работает.

Sticky sessions (если несколько Node.js):

upstream nodejs {
    ip_hash;   # один клиент → один сервер
    server 127.0.0.1:3001;
    server 127.0.0.1:3002;
}

Правило: для WebSocket — sticky, или Redis adapter в Socket.IO.

Кэширование на Gateway

proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=api:10m max_size=1g;

location /api/public/ {
    proxy_cache api;
    proxy_cache_valid 200 5m;
    proxy_cache_key "$scheme$request_method$host$request_uri";

    proxy_pass http://laravel;
    add_header X-Cache-Status $upstream_cache_status;
}

Что даёт: публичные GET-запросы кэшируются на Nginx.

Практика: настроить Gateway

1. Nginx-конфиг:

upstream laravel { server 127.0.0.1:9000; }
upstream nodejs { server 127.0.0.1:3000; }

server {
    listen 443 ssl http2;
    server_name api.example.com;

    ssl_certificate /etc/letsencrypt/live/api.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/api.example.com/privkey.pem;

    limit_req_zone $binary_remote_addr zone=api:10m rate=100r/s;

    location /api/ {
        limit_req zone=api burst=200 nodelay;
        fastcgi_pass laravel;
        include fastcgi_params;
    }

    location /ws/ {
        proxy_pass http://nodejs;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_read_timeout 3600s;
    }

    location /storage/ {
        alias /var/www/app/storage/app/public/;
        expires 30d;
    }

    location /health {
        access_log off;
        return 200 'ok';
    }
}

2. Проверить:

nginx -t
systemctl reload nginx
curl -I https://api.example.com/health

3. Проверить WebSocket:

websocat wss://api.example.com/ws

Частые ошибки

1. WebSocket без Upgrade:

404 или 400 на handshake

Решение: заголовки Upgrade и Connection.

2. CORS + credentials:

Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true

Решение: конкретный домен, не *.

3. Один upstream на все сервисы:

Laravel и Node.js на одном порту

Решение: разные upstream, разные location.

4. Нет таймаута для WS:

Соединение рвётся через 60 сек

Решение: proxy_read_timeout 3600s.

5. Забыли health-check:

Load balancer не знает, что сервис упал

Решение: /health endpoint.

6. Нет rate limit:

DDoS кладёт сервис

Решение: limit_req_zone.

Итог

  • Gateway — единая точка входа.
  • Nginx — обычно хватает.
  • Маршрутизация по путям: /api, /ws, /ai.
  • Авторизация — на Laravel или Redis.
  • Rate limiting — обязательно.
  • WebSocket — Upgrade + Connection + большой timeout.
  • Статика — отдавать напрямую.
  • Health-check — для мониторинга.
  • CORS — конкретный origin.

Дальше — RabbitMQ.