TriFrost

TriFrost Docs

Learn and build with confidence, from a first project with workers to global scale on bare metal.

Middleware Chains

In TriFrost, middleware is a first-class, typed, and deterministic execution model.

Unlike most frameworks, TriFrost does not use the onion model. There is no next(), no wrapping, no โ€œbefore/afterโ€ dance.

Instead, it uses a linear, top-down chain:

  • Each middleware runs in order
  • If it returns a response โ†’ the chain stops
  • Otherwise โ†’ the next handler runs
  • Middleware is automatically span-wrapped

This makes reasoning about execution clear, predictable, composable as well as observable.


Key Principles

  • No next(): middleware is not a stack
  • Linear execution: top-to-bottom, no recursion
  • Short-circuiting: middleware can stop the chain
  • Typed ctx.state: middleware can safely extend context
  • Scoped chains: middleware can be attached at any level: app, group, or route

๐Ÿ“š What is Middleware?

Itโ€™s just a function:

(ctx) => {
  if (!ctx.headers.get('x-api-key')) {
    return ctx.text('Forbidden', 403);
  }
}

If it returns undefined or the ctx, the chain continues.

If it returns a response, sets a 400+ status, or calls ctx.abort(), execution halts immediately.


๐Ÿงฌ Expanding State

If you want to pass data down the chain (like a user or session), use ctx.setState(...).

const requireUser = async <State extends Record<string, unknown>>(ctx: Context<State>) => {
  const user = await getUserFromToken(ctx);
  if (!user) return ctx.status(401);

  return ctx.setState({ user });
};

This is required to propagate new types into ctx.state.

Now all downstream middleware and routes know ctx.state.user is defined, and typed.

๐Ÿ‘‰ Also see: Context & State Management | Middleware Basics

Manual Typing (Advanced)

If you must call ctx.setState(...) somewhere in the middle and canโ€™t return it (e.g., logging after setting), you can annotate the return manually:

async function m <S extends Record<string, unknown>> (
	ctx: Context<S>
): Promise<Context<S & {foo: string}>> {
  ctx.setState({foo: 'bar'});
  ...
  return ctx;
};

But whenever possible return ctx.setState(...) is the cleanest and most type-safe way.


๐Ÿ” Typing the Chain

Each middleware explicitly defines what it requires and what it adds:

const auth <State>(ctx: Context<State>) => {
  const user = ...;
  return ctx.setState({ user });
};

const onlyAdmins <State extends {user: User}>(ctx: Context<State>) => {
  if (ctx.state.user.role !== 'admin') {
    return ctx.status(403);
  }
};

If you add onlyAdmins() without auth() before it in your chain, TypeScript will yell at you.

โœ… Type safety isn't optional โ€” itโ€™s baked into the chain.

๐Ÿ‘‰ Also see: Context & State Management | Middleware Basics


๐Ÿ”— Composing Chains

You can attach middleware:

  • To the app
  • To a router or group
  • To a single route

They stack in order:

app
  .use(auth)         // adds ctx.state.user
  .use(onlyAdmins)   // requires ctx.state.user
  .get('/admin', ctx => ctx.json(ctx.state.user));
app
  .get('/home', ctx => ctx.text('hello world')) // no middleware here
  .use(auth)         // adds ctx.state.user
  .use(onlyAdmins)   // requires ctx.state.user
  .get('/admin', ctx => ctx.json(ctx.state.user));

This is deterministic. Thereโ€™s no โ€œbefore/afterโ€ guesswork.


๐Ÿ›‘ Halting the Chain

Any middleware can short-circuit the chain by:

  • Returning a response through eg: ctx.text(), ctx.json(), ctx.status(), etc.
  • Calling ctx.abort() if in dire need
  • Returning a status >= 400 using return ctx.setStatus(...)

Once any of these happen, downstream handlers wonโ€™t run.

const blockUnauthenticated = (ctx) => {
  if (!ctx.state.user) return ctx.setStatus(401);
};

๐Ÿงฑ Example: Scoping

app
  .use(cors()) // all routes
  .group('/admin', admin => {
    admin
      .use(requireUser)
      .use(onlyAdmins)
      .get('/dashboard', ctx => ctx.text('welcome admin'));
  });
  • cors() applies to everything
  • /admin/dashboard runs cors โ†’ requireUser โ†’ onlyAdmins โ†’ handler
  • Outside /admin, those checks are skipped

โš’๏ธ Built-In Middleware

TriFrost includes a powerful set of built-ins, just .use() them:

All of them follow the same model: deterministic, typed, halt-or-continue.


Mental Model

TriFrost middleware is not a sandwich.

There is no onion. No unwind. No next().

Itโ€™s a pipeline:

[ A ] โ†’ [ B ] โ†’ [ C ] โ†’ handler
         โ›”๏ธ (halts if returns)

๐Ÿ‘‰ Also see: Request & Response Lifecycle


Best Practices

  • โœ… Use .setState() to share state downstream
  • โœ… Use type constraints to enforce chain correctness
  • โœ… Attach middleware at the level it applies: app/group/route
  • โœ… Let middleware short-circuit deliberately (401, 429, etc.)
  • โœ… Keep each piece focused: one job per middleware
  • โœ… Use Context<Env, State> to lock in types across files

TLDR

  • No next(), no onion model
  • Top-down, linear execution
  • Middleware halts chain if it returns a response
  • ctx.setState() is how you extend context
  • TypeScript knows your state chain, and enforces it
  • Middleware can run globally, per group, or per route
  • Middleware can require a state shape and extend the state

Next Steps

Loved the read? Share it with others