При разработке современных веб-приложений часто возникает необходимость перенаправлять запросы от клиента к внешним API, внутренним сервисам или микросервисной инфраструктуре. Во время локальной разработки такая задача особенно актуальна из-за ограничений браузера, связанных с политикой одного источника (Same-Origin Policy), различий между доменами, портами и протоколами.
Проксирование запросов через middleware позволяет:
В экосистеме Esbuild проксирование обычно организуется через встроенный сервер разработки либо через собственные middleware-обработчики, подключаемые к HTTP-серверу.
Сам по себе Esbuild является сборщиком (bundler) и транспилятором. Он не предоставляет сложную систему middleware наподобие Express.js. Однако благодаря API плагинов и встроенному серверу разработки можно внедрять промежуточные обработчики, которые перехватывают запросы до их обработки конечным сервером.
Типичная схема выглядит следующим образом:
Браузер
↓
Middleware
↓
Прокси
↓
Внешний API
Каждый запрос проходит через промежуточный слой, где может быть:
Рассмотрим ситуацию:
Frontend работает по адресу:
http://localhost:3000
Backend API расположен на другом порту:
http://localhost:8080
Запрос:
fetch("http://localhost:8080/api/users");
может столкнуться с ошибкой CORS:
Access to fetch at 'http://localhost:8080'
from origin 'http://localhost:3000'
has been blocked by CORS policy
Вместо обращения напрямую к API используется прокси:
fetch("/api/users");
Middleware принимает запрос:
GET /api/users
и автоматически перенаправляет его на:
http://localhost:8080/api/users
Для браузера запрос выглядит локальным, поэтому проблема CORS исчезает.
Прокси-сервер работает как посредник между клиентом и целевым сервисом.
Схема взаимодействия:
Client
|
v
Proxy Middleware
|
v
Target API
Последовательность действий:
Например:
GET /api/products
Преобразуется в:
GET https://api.company.com/products
Часто Esbuild используется совместно со встроенным HTTP-сервером Node.js.
Простейший пример:
import http from "http";
const server = http.createServer((req, res) => {
console.log(req.url);
res.end("Hello");
});
server.listen(3000);
Добавим промежуточный слой:
function middleware(req, res, next) {
console.log("Request:", req.method, req.url);
next();
}
Использование:
const server = http.createServer((req, res) => {
middleware(req, res, () => {
res.end("OK");
});
});
Теперь каждый запрос проходит через middleware.
Современный Node.js содержит встроенный Fetch API.
Проксирование можно реализовать следующим образом:
import http from "http";
const server = http.createServer(async (req, res) => {
if (req.url.startsWith("/api")) {
const response = await fetch(
`https://jsonplaceholder.typicode.com${req.url}`
);
const data = await response.text();
res.writeHead(response.status, {
"Content-Type":
response.headers.get("content-type") || "application/json"
});
res.end(data);
return;
}
res.end("Frontend");
});
server.listen(3000);
Запрос:
GET /api/posts
будет автоматически отправлен на:
https://jsonplaceholder.typicode.com/api/posts
Часто необходимо удалять префиксы маршрутов.
Исходный URL:
/api/users
Целевой API ожидает:
/users
Решение:
const targetPath = req.url.replace(/^\/api/, "");
Использование:
const response = await fetch(
`https://backend.local${targetPath}`
);
Результат:
/api/users
превращается в:
/users
Middleware позволяет автоматически дополнять запросы служебными данными.
Например:
const response = await fetch(url, {
headers: {
Authorization: "Bearer TOKEN",
"X-Application": "frontend"
}
});
После этого все запросы содержат дополнительные заголовки.
Полезные сценарии:
Прокси должен поддерживать не только GET-запросы.
Чтение тела запроса:
async function readBody(req) {
const chunks = [];
for await (const chunk of req) {
chunks.push(chunk);
}
return Buffer.concat(chunks);
}
Использование:
const body = await readBody(req);
const response = await fetch(targetUrl, {
method: req.method,
body
});
Теперь поддерживаются:
POST
PUT
PATCH
DELETE
Для корректной работы некоторых API необходимо передавать исходные заголовки.
Пример:
const response = await fetch(targetUrl, {
method: req.method,
headers: req.headers
});
Особенно важно сохранять:
Authorization
Cookie
Accept
Content-Type
Некоторые заголовки нельзя передавать напрямую.
Например:
const headers = { ...req.headers };
delete headers.host;
delete headers.connection;
delete headers["content-length"];
Далее:
await fetch(targetUrl, {
method: req.method,
headers
});
Это предотвращает ошибки маршрутизации и конфликты между серверами.
После получения ответа от удалённого сервера необходимо вернуть его браузеру.
Пример:
const data = await response.arrayBuffer();
res.writeHead(response.status);
res.end(Buffer.from(data));
Более полный вариант:
res.writeHead(response.status, {
"Content-Type":
response.headers.get("content-type")
});
Middleware часто используется для мониторинга.
Пример:
function logger(req, res, next) {
const start = Date.now();
res.on("finish", () => {
console.log(
req.method,
req.url,
Date.now() - start,
"ms"
);
});
next();
}
Вывод:
GET /api/users 25 ms
POST /api/login 78 ms
Удалённый сервер может быть недоступен.
Необходимо перехватывать ошибки:
try {
const response = await fetch(targetUrl);
const data = await response.text();
res.end(data);
} catch (error) {
res.statusCode = 502;
res.end("Bad Gateway");
}
Код:
502 Bad Gateway
традиционно используется для ошибок промежуточного прокси-сервера.
Крупные проекты часто работают сразу с несколькими сервисами.
Пример маршрутизации:
const routes = {
"/api": "http://localhost:8080",
"/auth": "http://localhost:9000",
"/files": "http://localhost:7000"
};
Поиск нужного сервера:
function resolveTarget(path) {
for (const prefix in routes) {
if (path.startsWith(prefix)) {
return routes[prefix];
}
}
return null;
}
Использование:
const target = resolveTarget(req.url);
Иногда структура маршрутов различается.
Исходный запрос:
/api/products
Требуемый адрес:
/v1/catalog/products
Middleware может выполнять преобразование:
const path = req.url.replace(
"/api",
"/v1/catalog"
);
Результат:
/api/products
↓
/v1/catalog/products
Middleware способен распределять нагрузку между несколькими узлами.
Список серверов:
const servers = [
"http://localhost:5001",
"http://localhost:5002",
"http://localhost:5003"
];
Алгоритм Round Robin:
let index = 0;
function nextServer() {
const server = servers[index];
index = (index + 1) % servers.length;
return server;
}
Каждый новый запрос будет направляться на следующий сервер.
Для снижения нагрузки можно сохранять результаты запросов.
Пример:
const cache = new Map();
Проверка:
if (cache.has(req.url)) {
res.end(cache.get(req.url));
return;
}
Сохранение:
cache.set(req.url, data);
Такой подход ускоряет работу при частых запросах к одним и тем же данным.
Хотя прокси обычно устраняет проблемы CORS, иногда требуется дополнительно управлять заголовками.
Пример:
res.setHeader(
"Access-Control-Allow-Origin",
"*"
);
res.setHeader(
"Access-Control-Allow-Methods",
"GET,POST,PUT,DELETE"
);
Для предварительных запросов:
if (req.method === "OPTIONS") {
res.statusCode = 204;
res.end();
return;
}
Во время разработки часто запускается процесс сборки и HTTP-сервер одновременно.
Пример запуска Esbuild:
import * as esbuild from "esbuild";
await esbuild.context({
entryPoints: ["src/index.js"],
bundle: true,
outfile: "dist/app.js"
});
Параллельно работает прокси-сервер:
server.listen(3000);
Структура проекта:
project
├── src
├── dist
├── build.js
└── server.js
Где:
build.js отвечает за сборку;server.js содержит middleware и проксирование.Плагины позволяют вмешиваться в процесс обработки ресурсов.
Простейший пример:
const plugin = {
name: "proxy-plugin",
setup(build) {
build.onResolve(
{ filter: /^api:/ },
args => {
return {
path: args.path
};
}
);
}
};
Регистрация:
await esbuild.build({
entryPoints: ["src/index.js"],
bundle: true,
plugins: [plugin]
});
Хотя подобные плагины чаще используются для обработки импортов, они могут участвовать в построении инфраструктуры взаимодействия с удалёнными сервисами.
При разработке проксирующего middleware необходимо учитывать ряд рисков.
Открытый прокси
Опасный вариант:
fetch(req.query.url);
Пользователь способен направлять запросы на любые ресурсы.
Лучше использовать белый список:
const allowedHosts = [
"api.company.com"
];
Проверка:
if (!allowedHosts.includes(hostname)) {
res.statusCode = 403;
return;
}
Защита внутренних сервисов
Нельзя публиковать адреса:
localhost
127.0.0.1
10.x.x.x
192.168.x.x
если прокси доступен извне.
Контроль размеров запросов
Для предотвращения перегрузки:
const MAX_SIZE = 5 * 1024 * 1024;
Следует ограничивать объём загружаемых данных.
import http from "http";
const TARGET =
"https://jsonplaceholder.typicode.com";
const server = http.createServer(
async (req, res) => {
try {
if (!req.url.startsWith("/api")) {
res.end("Frontend");
return;
}
const path =
req.url.replace(/^\/api/, "");
const response = await fetch(
TARGET + path,
{
method: req.method,
headers: req.headers
}
);
const data =
await response.arrayBuffer();
res.writeHead(
response.status,
{
"Content-Type":
response.headers.get(
"content-type"
)
}
);
res.end(Buffer.from(data));
} catch (error) {
res.statusCode = 502;
res.end("Proxy error");
}
}
);
server.listen(3000);
Такой middleware реализует основные возможности прокси-сервера: