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>;

Де реєструвати

Дві області — глобально (кожен запит) та на маршрут:

// 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.