Введение: почему вашему проекту нужен API Gateway?
Представьте: вы пишете стартап или корпоративное приложение на TypeScript. У вас уже есть сервис пользователей, сервис заказов и сервис уведомлений. Каждый из них живет на своём порту (или даже на своём сервере), общается по HTTP, использует свою базу данных. Всё работает, но клиент (фронтенд, мобильное приложение) начинает задыхаться: чтобы собрать страницу профиля, ему нужно сделать 4-5 запросов к разным сервисам, разобраться с разными форматами ошибок, а ещё авторизоваться в каждом.
Тут на сцену выходит API Gateway — единая точка входа для всех клиентов. Он берёт на себя маршрутизацию, аутентификацию, агрегацию данных, кэширование и даже трансформацию ответов. В этой статье мы напишем настоящий API Gateway на TypeScript с нуля, разберём паттерны и антипаттерны, и покажем, как не наступить на грабли.
Что такое API Gateway простыми словами
API Gateway — это прослойка между клиентом и вашими внутренними сервисами. Клиент (браузер, мобилка, внешний API) стучится только в Gateway. Gateway смотрит на путь запроса, проверяет токен, и перенаправляет запрос в нужный микросервис. Иногда он даже собирает ответ из нескольких сервисов в один — это называется агрегацией.
Зачем это нужно в TypeScript экосистеме? Потому что TypeScript даёт нам типы, интерфейсы и строгость, а Gateway — это идеальное место для валидации и приведения данных к единому контракту.
Архитектура: как это работает на TypeScript
Давайте спроектируем простую, но рабочую архитектуру. У нас будет:
- Gateway (наш TypeScript сервис) — порт 3000, единственная точка входа.
- Сервис пользователей (User Service) — порт 4001, REST API.
- Сервис заказов (Order Service) — порт 4002, REST API.
- Сервис уведомлений (Notification Service) — порт 4003, REST API.
Все внутренние сервисы живут в своей сети (например, Docker network) и не видны извне. Клиент общается только с Gateway.
// Структура проекта
gateway/
├── src/
│ ├── routes/
│ │ ├── userRoutes.ts
│ │ ├── orderRoutes.ts
│ │ └── notificationRoutes.ts
│ ├── middleware/
│ │ ├── authMiddleware.ts
│ │ ├── rateLimiter.ts
│ │ └── cacheMiddleware.ts
│ ├── services/
│ │ └── proxyService.ts
│ ├── types/
│ │ └── index.ts
│ └── index.ts
├── package.json
└── tsconfig.json
Пишем базовый API Gateway на TypeScript
Начнём с установки зависимостей. Нам понадобится Express (самый популярный фреймворк), http-proxy-middleware для проксирования, и несколько утилит.
npm init -y
npm install express http-proxy-middleware cors helmet express-rate-limit
npm install -D typescript @types/node @types/express ts-node nodemon
Создадим tsconfig.json:
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
}
Теперь напишем основной файл src/index.ts:
import express, { Request, Response } from 'express';
import cors from 'cors';
import helmet from 'helmet';
import { createProxyMiddleware } from 'http-proxy-middleware';
import { authMiddleware } from './middleware/authMiddleware';
import { rateLimiter } from './middleware/rateLimiter';
const app = express();
const PORT = process.env.PORT || 3000;
// Глобальные middleware
app.use(helmet());
app.use(cors());
app.use(express.json());
app.use(rateLimiter);
// Прокси для сервиса пользователей
app.use('/api/users', authMiddleware, createProxyMiddleware({
target: 'http://user-service:4001',
changeOrigin: true,
pathRewrite: {
'^/api/users': '/users'
}
}));
// Прокси для сервиса заказов
app.use('/api/orders', authMiddleware, createProxyMiddleware({
target: 'http://order-service:4002',
changeOrigin: true,
pathRewrite: {
'^/api/orders': '/orders'
}
}));
// Прокси для сервиса уведомлений
app.use('/api/notifications', authMiddleware, createProxyMiddleware({
target: 'http://notification-service:4003',
changeOrigin: true,
pathRewrite: {
'^/api/notifications': '/notifications'
}
}));
// Health check
app.get('/health', (_req: Request, res: Response) => {
res.json({ status: 'OK', timestamp: new Date().toISOString() });
});
app.listen(PORT, () => {
console.log(`API Gateway запущен на порту ${PORT}`);
});
Обратите внимание на pathRewrite. Мы режем префикс /api/users и передаём во внутренний сервис просто /users. Это стандартная практика — Gateway выступает как "швейцарский нож" для трансформации путей.
Аутентификация: единый JWT для всех сервисов
Самая частая задача Gateway — проверка токена. Мы не хотим, чтобы каждый микросервис дёргал базу для проверки JWT. Давайте напишем middleware, который валидирует токен и прокидывает payload в заголовки.
Создаём src/middleware/authMiddleware.ts:
import { Request, Response, NextFunction } from 'express';
import jwt from 'jsonwebtoken';
const JWT_SECRET = process.env.JWT_SECRET || 'super-secret-key';
interface JwtPayload {
userId: string;
role: string;
}
export const authMiddleware = (req: Request, res: Response, next: NextFunction): void => {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
res.status(401).json({ error: 'Отсутствует токен авторизации' });
return;
}
const token = authHeader.split(' ')[1];
try {
const decoded = jwt.verify(token, JWT_SECRET) as JwtPayload;
// Прокидываем данные пользователя в заголовки для внутренних сервисов
req.headers['x-user-id'] = decoded.userId;
req.headers['x-user-role'] = decoded.role;
next();
} catch (error) {
res.status(401).json({ error: 'Невалидный или просроченный токен' });
}
};
Теперь все внутренние сервисы могут просто читать заголовки x-user-id и x-user-role — без лишних запросов к базе. Это сильно ускоряет работу и уменьшает нагрузку.
Rate Limiting: защита от ботов и DDoS
Даже самый крутой TypeScript проект может лечь от одного скрипта, который долбит API. По