Search docs

Jump between documentation pages.

Browse docs

Middleware combinators

DaloyJS exposes three composition primitives for Hooks bundles. Use every() to package a full stack, some() to accept any one valid proof, and except() to skip a gate for specific paths.

every(): run a whole stack in order

every(...layers) merges several Hooks bundles into one that runs each layer in registration order. It is equivalent to calling app.use(...) for each bundle, but lets you name and reuse a curated stack. All lifecycle phases compose: onRequest and onResponse run in order, beforeHandle and onError stop on the first Response, and afterHandle plus onSend thread the value through each layer.

every() runs each layer in order
  1. 01layer 1requestId()tag the request
  2. 02layer 2bearerAuth()verify the token
  3. 03layer 3rateLimit()throttle the shared admin bucket
  4. 04resultHandlerone reusable adminStack value
Symbol-keyed security markers such as CORS, CSRF, session, and secure-headers are forwarded onto the merged bundle so boot-time guards still see them.
ts
import { App, every, requestId, bearerAuth, rateLimit } from "@daloyjs/core";

const app = new App();

const adminStack = every(
  requestId(),
  bearerAuth({
    realm: "admin",
    validate: (token) => token === process.env.ADMIN_TOKEN,
  }),
  rateLimit({ windowMs: 60_000, max: 30, groupId: "admin" }),
);

app.use(adminStack);

some(): accept any one proof of identity

some(...layers) runs each bundle's beforeHandle in order and accepts the request as soon as one bundle passes without throwing or returning a Response. Use it for routes that accept more than one credential style, such as a bearer token or a signed session cookie.

some() accepts any one proof
requestbeforeHandle runs each bundle in orderfirst to pass wins
proof AbearerAuth()valid bearer token
proof Bsession authsigned cookie with a user id
acceptedFirst passing bundle winsits ctx.state and headers are preserved
A bundle that returns a Response counts as a denial and the next bundle gets a chance. If every bundle denies, the first denial wins.
ts
import { App, every, some, bearerAuth, session } from "@daloyjs/core";

const app = new App();

const sessionAuth = every(
  session({ secret: process.env.SESSION_SECRET! }),
  {
    beforeHandle(ctx) {
      const sessionState = ctx.state.session as
        | { data?: Record<string, unknown> }
        | undefined;

      if (typeof sessionState?.data?.userId === "string") return;

      return new Response("Unauthorized", { status: 401 });
    },
  },
);

app.use(
  some(
    bearerAuth({
      realm: "api",
      validate: (token) => token === process.env.PUBLIC_API_TOKEN,
    }),
    sessionAuth,
  ),
);

Semantics worth knowing:

  • The first bundle that resolves without throwing or returning a Response wins. Its context mutations, including headers and ctx.state, are preserved.
  • A bundle that returns a Response is treated as a denial, and the next bundle gets a chance. If every bundle returns a denial, the first Response is sent.
  • When every bundle fails and the first denial was a thrown error, that error is rethrown. Put the auth method whose status and WWW-Authenticate challenge you want clients to see first.
  • Only the beforeHandle selection strategy changes. afterHandle, onSend, onResponse, and onError from every bundle still compose normally.

except(): apply everywhere but a few paths

except(when, hooks) runs a bundle on every request except those matching when. The canonical use is applying auth everywhere except health checks, OpenAPI JSON, and docs assets.

ts
import { App, except, bearerAuth } from "@daloyjs/core";

const app = new App();

app.use(
  except(
    ["/health", "/openapi.json", "/docs/**"],
    bearerAuth({ validate: (token) => token === process.env.API_TOKEN }),
  ),
);

The when matcher accepts:

  • A path string starting with /. * matches one path segment with no slash, and ** matches any suffix.
  • An array of path patterns.
  • A predicate function that receives the request context and returns true to skip the gated bundle.

Path matching uses the same new URL(request.url).pathname view the router sees. It is case-sensitive, exact for non-wildcard patterns, and does not add a rewrite or extra decode step. That makes exemptions fail closed for case changes, trailing-slash mismatches, and encoded traversal tricks.

Only the wrapped bundle's beforeHandle phase is skipped. Its onRequest, afterHandle, onSend, and onResponse phases still run. Wrap each bundle with except() individually if you need to gate different phases with different rules.

Composing the three

The primitives nest. A common production shape is to run correlation, secure headers, and authentication everywhere, but skip the auth gate for health and documentation endpoints.

ts
import {
  App,
  every,
  except,
  requestId,
  secureHeaders,
  bearerAuth,
} from "@daloyjs/core";

const app = new App();

const protectedStack = every(
  requestId(),
  secureHeaders(),
  except(
    ["/health", "/openapi.json", "/docs/**"],
    bearerAuth({ validate: (token) => token === process.env.API_TOKEN }),
  ),
);

app.use(protectedStack);