Пошук уроків, статей та іншого контенту
Дослідите єдину точку входу для API, маршрутизацію, автентифікацію, лімітування та агрегацію запитів.
API Gateway — це єдина точка входу для клієнтів до набору backend-сервісів.
Замість того щоб клієнт безпосередньо звертався до кожного мікросервісу:
Мобільний застосунок ──► User Service
├──► Order Service
└──► Catalog Serviceзапити проходять через gateway:
Мобільний застосунок ──► API Gateway ──► User Service
├──► Order Service
└──► Catalog ServiceGateway приймає зовнішній HTTP-запит, визначає потрібний сервіс і виконує спільні для API операції:
маршрутизацію;
перевірку автентифікації;
лімітування частоти запитів;
агрегацію кількох запитів;
перетворення заголовків або формату відповіді;
централізоване логування та моніторинг.
API Gateway не повинен містити основну бізнес-логіку. Його завдання — керувати проходженням запитів між клієнтами та внутрішніми сервісами.
Без gateway клієнти мають знати:
адреси всіх сервісів;
структуру внутрішнього API;
правила автентифікації кожного сервісу;
спосіб обробки помилок різних сервісів.
Це створює сильну залежність клієнта від внутрішньої архітектури.
Gateway приховує внутрішню структуру системи:
Клієнт:
GET /api/dashboard/42
Gateway:
GET http://user-service/users/42
GET http://order-service/orders?userId=42Для клієнта система виглядає як одне API, навіть якщо всередині працює багато сервісів.
Запит через gateway зазвичай проходить такі етапи:
Gateway отримує HTTP-запит.
Визначає клієнта та його IP-адресу.
Перевіряє автентифікаційні дані.
Перевіряє ліміт запитів.
Визначає маршрут.
Перенаправляє запит у потрібний сервіс.
За потреби об'єднує відповіді кількох сервісів.
Повертає клієнту HTTP-відповідь.
Запит
│
├── Автентифікація
│
├── Лімітування
│
├── Маршрутизація
│
├── Виклик одного або кількох сервісів
│
└── ВідповідьМаршрутизація визначає, у який backend-сервіс потрібно передати запит.
Наприклад:
| Зовнішній маршрут | Внутрішній сервіс | |---|---| | /api/users/* | User Service | | /api/orders/* | Order Service | | /api/products/* | Product Service |
У production-системах маршрути можуть залежати від:
HTTP-методу;
шляху;
версії API;
заголовків;
регіону користувача;
версії backend-сервісу.
Gateway може направляти різні версії API до різних реалізацій:
/api/v1/users ──► User Service v1
/api/v2/users ──► User Service v2Це дає змогу оновлювати внутрішні сервіси, не змінюючи одразу всіх клієнтів.
Не варто дозволяти клієнту передавати довільну адресу сервісу:
GET /proxy?url=http://internal-serviceТакий підхід може створити SSRF-вразливість: зловмисник змусить gateway звернутися до внутрішніх або захищених ресурсів.
Краще використовувати заздалегідь визначену таблицю маршрутів:
const routes = {
users: "http://localhost:4001",
orders: "http://localhost:4002"
};Gateway часто є першим місцем, де перевіряється автентифікація клієнта.
Наприклад, клієнт надсилає:
Authorization: Bearer demo-tokenGateway може:
перевірити наявність заголовка;
перевірити формат токена;
перевірити підпис і термін дії JWT;
визначити ідентифікатор користувача;
передати інформацію про користувача внутрішньому сервісу.
Якщо автентифікація не пройдена, gateway повертає:
401 UnauthorizedАвтентифікація відповідає на запитання: «Хто це?»
Авторизація відповідає на запитання: «Що цій особі дозволено?»
Gateway може централізовано перевірити токен, але перевірку доступу до конкретного ресурсу часто повинен виконувати сам сервіс.
Наприклад:
gateway перевірив, що користувач увійшов у систему;
Order Service перевірив, що цей користувач має право переглядати конкретне замовлення.
Не слід вважати перевірку на gateway єдиним захистом внутрішніх сервісів. Сервіси також повинні довіряти лише автентифікованим внутрішнім запитам і, за потреби, повторно перевіряти дозволи.
Після перевірки токена gateway може додати внутрішній заголовок:
X-User-Id: 42Важливо, щоб gateway спочатку видалив такий заголовок від клієнта, а потім встановив власне значення. Інакше клієнт зможе підробити ідентичність користувача.
Поганий порядок:
1. Прийняти X-User-Id від клієнта
2. Передати його сервісу
3. Перевірити AuthorizationБезпечніший порядок:
1. Видалити зовнішній X-User-Id
2. Перевірити Authorization
3. Визначити користувача
4. Додати власний X-User-Id
5. Передати запит сервісуRate limiting обмежує кількість запитів за певний проміжок часу.
Наприклад:
Не більше 100 запитів за хвилину для одного користувачаЛімітувати можна за:
IP-адресою;
ідентифікатором користувача;
API-ключем;
клієнтським застосунком;
конкретним маршрутом.
Якщо ліміт перевищено, gateway повертає:
429 Too Many RequestsКорисно також надсилати клієнту інформацію про ліміт:
Retry-After: 30Це повідомляє, через скільки секунд можна повторити запит.
Один із простих алгоритмів — фіксоване вікно:
Ліміт: 5 запитів за 60 секунд
12:00:00–12:00:59 — максимум 5 запитів
12:01:00–12:01:59 — лічильник починається зновуТакий підхід легко реалізувати, але він має недолік: на межі двох вікон клієнт може виконати майже подвійну кількість запитів за короткий час.
Для розподіленої системи лічильник не можна надійно зберігати лише в пам'яті одного gateway. Якщо gateway працює в кількох екземплярах, потрібне спільне сховище або спеціалізований компонент лімітування.
Іноді один екран клієнта потребує даних із кількох сервісів.
Без агрегації клієнт може виконати:
GET /api/users/42
GET /api/orders?userId=42
GET /api/notifications?userId=42Gateway може надати один endpoint:
GET /api/dashboard/42і всередині паралельно звернутися до кількох сервісів:
GET /users/42
GET /orders?userId=42
GET /notifications?userId=42Переваги:
менше мережевих запитів між клієнтом і системою;
простіший клієнтський код;
єдиний формат відповіді;
можливість виконувати внутрішні запити паралельно.
Якщо запити незалежні один від одного, їх варто запускати паралельно, а не послідовно.
const [user, orders] = await Promise.all([
getUser(userId),
getOrders(userId)
]);Потрібно заздалегідь визначити поведінку, якщо один із сервісів недоступний:
повертати помилку для всього endpoint;
повертати часткову відповідь;
використовувати кешовані дані;
позначати окреме поле як недоступне.
Для критичних даних зазвичай повертають помилку всього запиту. Для другорядних даних може бути прийнятною часткова відповідь.
Нижче наведено самодостатній приклад для Node.js 18 або новішої версії. Він запускає:
User Service на порту 4001;
Order Service на порту 4002;
API Gateway на порту 3000.
Gateway реалізує:
маршрутизацію;
перевірку Bearer-токена;
просте лімітування;
агрегацію даних для dashboard endpoint.
const http = require("node:http");
const PORT = 3000;
const USER_SERVICE_URL = "http://localhost:4001";
const ORDER_SERVICE_URL = "http://localhost:4002";
const rateLimit = new Map();
const RATE_LIMIT = 10;
const RATE_WINDOW_MS = 60_000;
function sendJson(response, statusCode, data, headers = {}) {
response.writeHead(statusCode, {
"Content-Type": "application/json; charset=utf-8",
...headers
});
response.end(JSON.stringify(data));
}
function getClientKey(request) {
return request.socket.remoteAddress || "unknown";
}
function isRateLimited(key) {
const now = Date.now();
const current = rateLimit.get(key);
if (!current || now >= current.resetAt) {
rateLimit.set(key, {
count: 1,
resetAt: now + RATE_WINDOW_MS
});
return false;
}
if (current.count >= RATE_LIMIT) {
return true;
}
current.count += 1;
return false;
}
function authenticate(request) {
const authorization = request.headers.authorization;
if (authorization !== "Bearer demo-token") {
return false;
}
return true;
}
async function fetchJson(url) {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Backend service returned ${response.status}`);
}
return response.json();
}
function startUserService() {
const server = http.createServer((request, response) => {
const url = new URL(request.url, `http://${request.headers.host}`);
const match = url.pathname.match(/^\/users\/([^/]+)$/);
if (request.method === "GET" && match) {
const userId = match[1];
sendJson(response, 200, {
id: userId,
name: "Olena Kovalenko",
email: "olena@example.com"
});
return;
}
sendJson(response, 404, { error: "User route not found" });
});
server.listen(4001, () => {
console.log("User Service: http://localhost:4001");
});
}
function startOrderService() {
const server = http.createServer((request, response) => {
const url = new URL(request.url, `http://${request.headers.host}`);
if (request.method === "GET" && url.pathname === "/orders") {
const userId = url.searchParams.get("userId");
sendJson(response, 200, {
userId,
orders: [
{ id: "order-101", total: 1250, status: "paid" },
{ id: "order-102", total: 890, status: "shipped" }
]
});
return;
}
sendJson(response, 404, { error: "Order route not found" });
});
server.listen(4002, () => {
console.log("Order Service: http://localhost:4002");
});
}
async function handleGatewayRequest(request, response) {
const url = new URL(request.url, `http://${request.headers.host}`);
if (request.method !== "GET") {
sendJson(response, 405, { error: "Only GET requests are supported" });
return;
}
const clientKey = getClientKey(request);
if (isRateLimited(clientKey)) {
sendJson(
response,
429,
{ error: "Too many requests" },
{ "Retry-After": "60" }
);
return;
}
if (!authenticate(request)) {
sendJson(response, 401, { error: "Unauthorized" });
return;
}
// Маршрут для отримання одного користувача.
const userMatch = url.pathname.match(/^\/api\/users\/([^/]+)$/);
if (userMatch) {
const userId = userMatch[1];
try {
const user = await fetchJson(
`${USER_SERVICE_URL}/users/${encodeURIComponent(userId)}`
);
sendJson(response, 200, user);
} catch (error) {
sendJson(response, 502, {
error: "User Service is unavailable"
});
}
return;
}
// Агрегований маршрут викликає два сервіси паралельно.
const dashboardMatch = url.pathname.match(/^\/api\/dashboard\/([^/]+)$/);
if (dashboardMatch) {
const userId = dashboardMatch[1];
try {
const [user, orders] = await Promise.all([
fetchJson(
`${USER_SERVICE_URL}/users/${encodeURIComponent(userId)}`
),
fetchJson(
`${ORDER_SERVICE_URL}/orders?userId=${encodeURIComponent(userId)}`
)
]);
sendJson(response, 200, {
user,
orders: orders.orders
});
} catch (error) {
sendJson(response, 502, {
error: "One of the backend services is unavailable"
});
}
return;
}
sendJson(response, 404, { error: "Gateway route not found" });
}
function startGateway() {
const server = http.createServer((request, response) => {
handleGatewayRequest(request, response).catch((error) => {
console.error(error);
sendJson(response, 500, { error: "Internal server error" });
});
});
server.listen(PORT, () => {
console.log(`API Gateway: http://localhost:${PORT}`);
console.log(
"Приклад запиту: curl -H \"Authorization: Bearer demo-token\" http://localhost:3000/api/dashboard/42"
);
});
}
startUserService();
startOrderService();
startGateway();Після запуску:
node gateway.jsможна виконати запит:
curl -H "Authorization: Bearer demo-token" \
http://localhost:3000/api/dashboard/42Gateway паралельно звернеться до двох внутрішніх сервісів і поверне об'єднану відповідь:
{
"user": {
"id": "42",
"name": "Olena Kovalenko",
"email": "olena@example.com"
},
"orders": [
{
"id": "order-101",
"total": 1250,
"status": "paid"
},
{
"id": "order-102",
"total": 890,
"status": "shipped"
}
]
}У прикладі токен навмисно простий. У реальній системі потрібно перевіряти справжні токени за правилами вашого механізму автентифікації.
Gateway залежить від доступності внутрішніх сервісів. Якщо сервіс не відповідає, gateway не повинен чекати необмежено довго.
Для кожного виклику потрібен тайм-аут. У сучасному Node.js його можна реалізувати через AbortSignal.timeout:
async function fetchJsonWithTimeout(url) {
const response = await fetch(url, {
signal: AbortSignal.timeout(3000)
});
if (!response.ok) {
throw new Error(`Backend returned ${response.status}`);
}
return response.json();
}Типові зовнішні статуси gateway:
401 — клієнт не автентифікований;
403 — клієнт автентифікований, але не має дозволу;
404 — маршрут не знайдено;
429 — перевищено ліміт;
502 — backend повернув некоректну або невдалу відповідь;
504 — backend не відповів вчасно;
500 — помилка в самому gateway.
Не слід повертати клієнту внутрішні stack trace, адреси сервісів або службові деталі помилок.
Gateway обробляє великий потік запитів, тому важливо:
запускати незалежні внутрішні запити паралельно;
встановлювати тайм-аути;
не виконувати важку бізнес-логіку;
обмежувати розмір вхідного запиту;
контролювати кількість одночасних запитів;
використовувати повторні спроби лише для безпечних операцій.
Автоматичні повторні спроби можуть погіршити ситуацію під час перевантаження. Особливо небезпечно повторювати операції, які змінюють дані, якщо вони не є ідемпотентними.
API Gateway централізує багато важливих операцій, але це створює ризик:
Gateway недоступний → клієнти не можуть звернутися до жодного сервісуУ production-середовищі зазвичай використовують:
кілька екземплярів gateway;
балансувальник навантаження;
health checks;
автоматичний перезапуск;
централізовані логи;
метрики затримки та помилок.
Gateway також не повинен зберігати важливий стан лише у власній пам'яті. Наприклад, in-memory rate limit із прикладу підходить для демонстрації, але не гарантує спільний ліміт між кількома екземплярами.
GET /proxy?url=https://any-host.exampleЦе може перетворити gateway на SSRF-проксі. Використовуйте лише заздалегідь дозволені маршрути.
Gateway не повинен безконтрольно передавати внутрішні секрети клієнта до backend-сервісів. Потрібно чітко визначити, які заголовки дозволені, а які мають бути видалені.
Якщо gateway починає обчислювати ціни, змінювати статуси замовлень або реалізовувати складні правила домену, він стає ще одним монолітним сервісом. Таку логіку краще залишати у відповідному backend-сервісі.
Погано:
const user = await getUser(id);
const orders = await getOrders(id);Якщо запити незалежні, це збільшує загальний час очікування. Краще використовувати Promise.all.
Без тайм-ауту повільний сервіс може зайняти всі ресурси gateway і спричинити каскадну відмову.
Таке лімітування не працює узгоджено, якщо gateway запущено в кількох процесах або контейнерах.
Не повертайте клієнту:
stack trace;
внутрішні URL;
назви баз даних;
секрети;
службові заголовки.
Клієнту достатньо стабільного формату помилки та коректного HTTP-статусу.
Gateway може перевірити токен, але сервіс усе одно повинен контролювати доступ до власних ресурсів.
API Gateway є єдиною зовнішньою точкою входу до кількох backend-сервісів.
Він може виконувати маршрутизацію, автентифікацію, лімітування та агрегацію.
Автентифікація визначає особу клієнта, а авторизація — його дозволи.
Для незалежних внутрішніх запитів варто використовувати паралельне виконання.
Rate limiting захищає систему від надмірної кількості запитів.
У розподіленому середовищі лічильники та інший стан не слід зберігати лише в пам'яті одного gateway.
Для викликів backend-сервісів потрібні тайм-аути та контроль помилок.
Gateway має координувати запити, але не поглинати бізнес-логіку сервісів.