Call Context
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>;
}
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);
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.
[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.
- Parses the body as JSON, once.
- 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.
- 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-errororinvalid-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 withserialization-error, so a request cannot change an object's prototype.prototypeandconstructorare ordinary keys. - Params nested too deep to decode or validate fail with
request-nesting-too-deep.