Fundamentals

Server context and middleware

Create request-scoped context and apply middleware globally or to selected routes.

Use server context for values shared by middleware and handlers during one request. Use middleware when a policy must run around handlers or stop a request with a contract-declared error. This page uses the shared contract from contract authoring; the authentication example additionally assumes that the contract declares an UNAUTHENTICATED error.

Create portable request context

defineServer() context receives a portable signal and matched route metadata. Return the application values needed during the request; the factory’s resolved return type becomes input.context.

TypeScript
import { defineServer } from '@hulla/api/server'
import { contract } from './contract'

const server = defineServer(contract, {
  context: ({ route, signal }) => ({
    requestId: crypto.randomUUID(),
    routeKey: route.key.join('.'),
    signal,
  }),
})

This context does not depend on Fetch or another host, so its implementations can be mounted through compatible adapters or called in process. Pass signal to cancellable work so downstream operations can stop when the request is cancelled.

Add native context only for host-specific needs

When application logic needs a native request value, create the adapter first and wrap the context factory with adapter.context(). This example reads an identity header; middleware and handlers receive the resulting userId, not the native Request.

TypeScript
import { defineServer } from '@hulla/api/server'
import { fetchAdapter } from '@hulla/api/fetch'
import { contract } from './contract'

const adapter = fetchAdapter()
const server = defineServer(contract, {
  context: adapter.context(({ request }) => ({
    userId: request.headers.get('x-user-id') ?? undefined,
  })),
})

Wrapping a context factory with adapter.context() binds every implementation from that server definition to the same adapter kind. Keep context portable unless handlers genuinely need native host data.

The adapter still owns request parsing and response writing. See server adapters for host-specific setup and adapter behavior for the behavior every adapter must preserve.

Apply middleware globally or to a route scope

Use server.middleware() to type middleware against the context and contract. Middleware receives signal, context, route, next, and declared errors when the contract defines them.

TypeScript
const logRequest = server.middleware(async ({ context, route, next }) => {
  console.info(context.userId, route.method, route.path)
  return next()
})

const requireUser = server.middleware(async ({ context, errors, next }) => {
  if (!context.userId) return errors.UNAUTHENTICATED()
  return next()
})

Both functions use the Fetch-backed server above, so context.userId is inferred. UNAUTHENTICATED must be declared in the contract, as assumed at the start of this page. Returning the declared error skips the remaining middleware and handler. Middleware and handlers may also throw a declared error. See error handling.

Apply middleware while building the implementation:

TypeScript
const implementation = server
  .use(logRequest)
  .use(contract.routes.users, requireUser)
  .implement({
    health: ({ response }) => response(200, 'ok'),
    users: {
      byId: ({ params, response }) =>
        params.id === 'ada'
          ? response(200, { id: 'ada', name: 'Ada' })
          : response(404, { message: 'User not found' }),
      rename: ({ params, body, response }) =>
        response(200, { id: params.id, name: body.name }),
    },
  })
RegistrationRoutes that run it
.use(middleware)Every route
.use(router, middleware)Every route below that router
.use(route, middleware)That route only

Calls to use() preserve declaration order. For a matched route, middleware wraps the next middleware and finally the handler: code before await next() runs on entry, and code after it resumes as the result returns outward. Separate registrations in overlapping scopes each run.

next() is single-use. Call it once to continue, or return a response or declared error to stop. Implementations created from child scopes retain middleware from those scopes; server fragments and composition shows how that inheritance behaves when fragments are mounted or composed.