Server

Call Context

The first parameter of every handler, and the request format mion uses.

Every route and middleware gets the call context as its first parameter. It holds everything about one request: the request, the response and the data your routes and middleware share.

Unlike Express, a route does not parse the request or write the response. It reads its params and returns a value, and mion sends both over HTTP.

Call Context Fields

The context is typed CallContext. To read headers, see Headers.

/** The call Context object passed as first parameter to any middleware or route */
export interface CallContext<ContextData extends Record<string, any> = any> {
  /** The route's URL path, like `/users/getUser` */
  readonly path: string;
  readonly request: MionRequest;
  readonly response: MionResponse;
  /** Shared by the chain's handlers, never sent back. */
  shared: ContextData;
  readonly executionChain: MethodsExecutionChain;
  /** The request limit this request was read against: the chain's number capped by the platform's */
  readonly maxBodySize: number;
  /** False on a not-found chain: the body is never read or parsed, `alwaysRun` middlewares still run */
  readonly readsBody: boolean;
  /** Query string from URL, used by the batch endpoint (`id=<batchId>`) and by query routes */
  readonly urlQuery?: string;
  /** Id of the batch a batch request is running */
  readonly batchId?: string;
  /** Route ids a batch request is running, in call order. Exposed for consumers (logging, metrics). */
  readonly batchRouteIds?: string[];
}

// ####### REQUEST & RESPONSE #######

/** Request body as the adapter hands it over: a JSON string, or an object a host already parsed */
export type RawRequestBody = string | AnyObject;

/** Router's own request object, do not confuse with the underlying raw request */
export interface MionRequest {
  readonly headers: Readonly<Omit<MionHeaders, 'append' | 'set' | 'delete'>>;
  readonly rawBody: RawRequestBody;
  readonly bodyType: SerializerCode;
  /** The parsed request body */
  readonly body: Readonly<AnyObject>;
  /** Errors outside the route's return type (validation, serialization, thrown), sent in `@thrownErrors` */
  readonly thrownErrors?: Readonly<Record<string, RpcError<string>>>;
}

/** Router's own response object, do not confuse with the underlying raw response */
export interface MionResponse {
  readonly statusCode: number;
  /** Set by the body serializer; the adapter writes it after every header, so no handler overrides it */
  readonly contentType: string;
  readonly headers: Readonly<MionHeaders>;
  /** The router response data, never to be modified by hand */
  readonly body: Readonly<ResponseBody>;
  /** true once something ended the execution chain: a thrown error or a returned FatalError */
  readonly hasErrors: boolean;
  /** The error that ended the chain (thrown or a returned FatalError), first one wins. An `alwaysRun` logger reads it here. */
  readonly fatalError?: RpcError<string>;
}
Never keep the context in a module-level variable or a cache. It holds the whole request body, so that memory is never freed.

Sharing Data Between Handlers

ctx.shared passes data from one middleware or route to the next in the same request. It is never sent to the client. The contextDataFactory router option is a function that creates it for each request, and its return type is the type of ctx.shared everywhere. Without a factory, ctx.shared is an empty object typed any.

import {createMionRouter, Routes} from '@mionjs/router';
import {getSessionUser, type AuthUser} from './myAuth.ts';

// without this return type, `me` would be typed as only `null`
const newSharedData = (): {me: AuthUser | null} => ({me: null});

const mion = createMionRouter({contextDataFactory: newSharedData});

const routes = {
  auth: mion.middleware(async (ctx): Promise<void> => {
    const cookie = ctx.request.headers.get('cookie');
    ctx.shared.me = (await getSessionUser(cookie)) ?? null;
  }),
  sayHello: mion.route((ctx): string => `Hello ${ctx.shared.me?.name}`), // written by auth above
} satisfies Routes;

export const api = mion.initRoutes(routes);
The factory must return a new object each time, never a module-level one, or all requests share it. A factory that returns an empty object makes initRoutes throw.

Request and Response Format

The URL path is the route id: the route's keys in your routes object, joined by /, after the basePath. The body is a JSON object whose keys are route and middleware ids, each holding an array of params. The response uses the same keys for the return values. A route or middleware with no params, or no return value, has no key.

import {createMionRouter, Routes} from '@mionjs/router';

const mion = createMionRouter();

const routes = {
  // takes a trace id and returns the time the server got the request
  trace: mion.middleware((ctx, traceId: string): number => Date.now()),
  users: {
    getUser: mion.route((ctx, id: number): {id: number; name: string} => ({
      id,
      name: 'Leo',
    })),
  },
} satisfies Routes;

export const api = mion.initRoutes(routes);

A call to users.getUser also sends the params of the trace middleware, and gets both return values back:

POST /users/getUser
{"trace": ["a1b2c3"], "users/getUser": [42]}

200 OK
{"trace": 1790812800000, "users/getUser": {"id": 42, "name": "Leo"}}

Headers go as HTTP headers, never in the body. Errors use their own keys, see Error Response Format.

The body can also be a plain JSON array of the route's params, like [42]. Use it for a quick curl call when no middleware needs params and you do not use mionSyncRoutes.

JSON Round Trip Steps

mion runs these steps for each request. A failed step ends the request with an error. To pick how params and return values are encoded, see Parser Strategies.

  1. Parses the body as JSON, once.
  2. For each middleware and route in the execution chain (the order they run in), turns its params back into their types (a Date from a string, a Map from an array), validates them, then runs it.
  3. Encodes every return value and sends the response.

Queries in the URL

The client sends a route made with mion.query() as a GET request when the URL fits in 4096 characters. The JSON body goes base64url encoded in the data query parameter. The server reads data only when the request has no body. A data value that is not base64url fails with invalid-query-body.

GET /users/getUser?data=eyJ1c2Vycy9nZXRVc2VyIjpbNDJdfQ

Request Body Safety

The router checks every body before your middleware and routes run:

  • The body must be valid JSON holding an object or an array. Anything else fails with parsing-json-request-error or invalid-request-body.
  • Each handler's value must be an array of params, or the request fails with serialization-error.
  • A __proto__ key in data typed as a record or index signature fails with serialization-error, so a request cannot change an object's prototype. prototype and constructor are ordinary keys.
  • Params nested too deep to decode or validate fail with request-nesting-too-deep.
Copyright © 2026