Middleware
Хуки before / after / wrap, що компонуються на сервері hopak().
Три хуки для конвеєра запитів. Типізовані функції, а не Koa-подібний ланцюг (ctx, next) — немає next(), який можна забути викликати.
type Before = (ctx: RequestContext) =>
Promise<Response | void> | Response | void;
type After = (
ctx: RequestContext,
result: { response?: Response; error?: unknown },
) => Promise<void> | void;
type Wrap = (
ctx: RequestContext,
run: () => Promise<Response>,
) => Promise<Response>;
Before— виконується перед хендлером. КиньтеHopakError, щоб перервати виконання з цим статусом. ПовернітьResponse, щоб перервати цією відповіддю. Поверніть нічого, щоб продовжити. Мутаціїctxпроходять далі (наприклад,ctx.user = ...). Правильне місце для авторизації, rate-limiting, request-id.After— виконується після хендлера (або помилки) з фінальною відповіддю. Не може змінити відповідь — тільки читання. Використовуйте для access-логів, метрик, аудиту. Якщо кидає помилку, помилка логується, а запит усе одно завершується.Wrap— обгортає виконання хендлера (плюсbeforeрівня маршруту).run()виробляє відповідь. Використовуйте тільки коли спостереження недостатньо — транзакції на запит, кеші в межах запиту, поширення correlation-id черезasync_hooks.
Де реєструвати
Дві області — глобально (кожен запит) та на маршрут:
// main.ts — global
import { hopak, requestId, requestLog } from '@hopak/core';
await hopak()
.before(requestId())
.after(requestLog())
.wrap(async (_ctx, run) => run()) // rarely needed
.listen();
// app/routes/api/posts.ts — per-route
export const POST = defineRoute({
before: [requireAuth()],
after: [audit],
handler: async (ctx) => { /* … */ },
});
Хелпери crud.* приймають ті ж опції другим аргументом — див.
CRUD → Gate a CRUD verb.
Порядок виконання
Для одного запиту:
global.before[] → wrap[] → route.before[] → handler
(throw or return Response short-circuits)
route.after[] → global.after[]
Wrap вкладаються — зовнішній виконується першим на вході й останнім
на виході (як шари цибулі).
hopak().before/.after/.wrap заморожується після listen()
Реєстрація middleware після старту сервера кидає помилку:
const app = hopak();
await app.listen();
app.before(requestId());
// Error: hopak().before(): cannot register middleware
// after listen() — add it before starting the server.
Це захищає від частково застосованих middleware на живих запитах.
Кожен запит проходить наскрізь
Змінено у 1.0
Глобальні middleware бачать кожен запит — включно зі статичними файлами, 404 та 405. Access-логи рахують промахи, а глобальний auth- чи rate-limit-guard закриває всю поверхню, а не лише зматчені маршрути.
Вбудовані: requestId() + requestLog()
Див. Recipe 23 для повного огляду. Одна команда вмикає обидва:
hopak use request-log.
Вбудований: rateLimit()
Додано у 1.0
Fixed-window, in-process rate limiter. Запити понад ліміт отримують 429 із заголовком Retry-After:
import { hopak, rateLimit } from '@hopak/core';
// global: 100 requests per minute per IP (the defaults)
await hopak().before(rateLimit()).listen();
// per route, custom key
export const POST = defineRoute({
before: [rateLimit({ max: 10, windowMs: 60_000, keyFor: (ctx) => ctx.user?.id ?? ctx.ip ?? '' })],
handler: async (ctx) => { /* … */ },
});
Стан живе у процесі — за load balancer-ом кожен інстанс рахує окремо; коли потрібен глобальний бюджет, ставте ліміт на edge.
EMPTY_MIDDLEWARE
Експортований sentinel для { before: [], after: [], wrap: [] }.
Корисно, якщо ви компонуєте власний об’єкт Middleware та хочете мати
явний порожній default.