Middleware
Declare a middleware with mion.middleware, one of the helpers the router returns. Middleware runs before or after a route and chains together into the route's Execution Chain.
Use it for the extra work a route needs, like authorization, rate limiting, CORS or logging. mion also ships ready-made middleware.
The first parameter is always the Call Context. The other parameters come from the client, decoded and validated before the middleware runs. To read or set HTTP headers, see Headers.
Defining a Middleware
import {createMionRouter, Routes} from '@mionjs/router';
import {myApp, getSharedData} from './full-example.app.ts';
const mion = createMionRouter({contextDataFactory: getSharedData});
const routes = {
logger: mion.middleware(
async (ctx): Promise<void> => {
// the error that ended the request, thrown or a returned FatalError
const fatal = ctx.response.fatalError;
if (fatal) await myApp.cloudLogs.error(ctx.path, fatal);
else myApp.cloudLogs.log(ctx.path, ctx.shared.me?.name);
},
// alwaysRun: the logger runs even after an error ended the request
{alwaysRun: true}
),
// ... other routes and middleware
} satisfies Routes;
Sharing a Middleware
Export its plain handler, and each app places it with mion.middleware(handler). Put its client installer in a separate entry point, so a client never imports the router.
Always Run
A thrown error or a returned FatalError ends the request: the remaining middleware and the route do not run. A plain returned RpcError stops nothing, see Error Handling.
A middleware with alwaysRun: true, like the logger above, runs anyway, on every request.
Requests That Already Failed
Only middleware that declares alwaysRun runs for a request that failed before its route could run. None of these requests is parsed, and the two 404s never read the body.
import {
createMionRouter,
addStartMiddlewares,
addEndMiddlewares,
} from '@mionjs/router';
const mion = createMionRouter();
// alwaysRun: a rate limiter must also see requests that already failed
const rateLimit = mion.rawMiddleware(
(ctx): void => {
console.log('incoming', ctx.path);
},
{alwaysRun: true}
);
// a global end middleware with alwaysRun sees every answer, a 404 and a 413 included
const accessLog = mion.rawMiddleware(
(ctx): void => {
console.log(ctx.path, ctx.response.statusCode);
},
{alwaysRun: true}
);
// no alwaysRun: no session is loaded for a request refused anyway
const loadSession = mion.rawMiddleware((ctx): void => {
console.log('loading the session for', ctx.path);
});
// registered BEFORE initRoutes: they are added to every chain the router builds
addStartMiddlewares({rateLimit, loadSession});
addEndMiddlewares({accessLog});
const sayHello = mion.route((ctx, name: string): string => `Hello ${name}`);
// declared beside the routes, so it never runs on either 404
const auth = mion.middleware((ctx): void => {
if (!ctx.request.headers.get('cookie')?.includes('session='))
throw new Error('unauthorized');
});
mion.initRoutes({auth, sayHello});
| Request | Answer |
|---|---|
| A path that names no route | 404, route-not-found |
| A batch id the server does not know | 404, batch-unknown-id |
| A body the platform adapter refused | 413, request-payload-too-large |
Raw Middleware
Use mion.rawMiddleware to access the platform's native request and response. It receives the CallContext, the raw request, the raw response and the RouterOptions. It gets no remote parameters and returns no data: it can only change the context and return or throw errors.
import {createMionRouter, Routes} from '@mionjs/router';
import {IncomingMessage, ServerResponse} from 'http';
const mion = createMionRouter();
type HttpRequest = IncomingMessage & {body: any};
const routes = {
progress: mion.rawMiddleware(
async (
ctx,
rawRequest: HttpRequest,
rawResponse: ServerResponse
): Promise<void> => {
return new Promise((resolve) => {
const maxTime = 1000;
const increment = 10;
let total = 0;
const intervale = setInterval(() => {
if (total >= maxTime) {
clearInterval(intervale);
resolve();
}
total += increment;
rawResponse.write(`\n${total}%`);
}, increment);
});
}
),
// ... other routes and middleware
} satisfies Routes;
Execution Chain
The Execution Chain is the list of middleware and the route that run on a route call: the middleware before the route, the route, then the middleware after it. It is built when the router starts, so it is the same for every request.
Everything in it runs in order, with no paths or conditions, unless an error occurs.
Execution Order
import {createMionRouter, Routes} from '@mionjs/router';
const mion = createMionRouter();
const routes = {
authorizationMiddleware: mion.middleware((): void => undefined),
users: {
userOnlyMiddleware: mion.middleware((): void => undefined), // runs only for routes under users
getUser: mion.route((): null => null),
setUser: mion.route((): null => null),
},
pets: {
getPet: mion.route((): null => null),
setPet: mion.route((): null => null),
},
errorHandlerMiddleware: mion.middleware((): void => undefined),
loggingMiddleware: mion.middleware((): void => undefined),
} satisfies Routes;
export const myValidApi = mion.initRoutes(routes);
Generated Execution Chain for: pets.getPet
Middleware Scope
A middleware can run on a subset of routes only. The userOnlyMiddleware above runs only for the routes under users, not those under pets.
Generated Execution Chain for: users.getUser
Returning Data Beside a Route
Use a middleware declared after a group's routes to return data beside each route's answer, like pagination.
import {isRpcError} from '@mionjs/core';
import {initClient} from '@mionjs/client';
import type {PaginationApi} from '../router/pagination.routes.ts';
const {routes} = initClient<PaginationApi>({baseURL: 'http://localhost:3000'});
// one call: the route's items, and the pagination middleware's answer at its path
const [products, error, response] = await routes.products.list(2).call();
const pagination = response.products?.pagination;
if (!error && pagination && !isRpcError(pagination))
console.log(products?.length, 'of', pagination.total);
import {createMionRouter, Routes} from '@mionjs/router';
export type Product = {id: string; name: string};
export type Pagination = {page: number; pageSize: number; total: number};
declare function findProducts(
page: number,
pageSize: number
): {items: Product[]; total: number};
const mion = createMionRouter({
contextDataFactory: (): {pagination?: Pagination} => ({}),
});
const routes = {
products: {
// returns only the items, and leaves the page info in the context
list: mion.route((ctx, page: number): Product[] => {
const {items, total} = findProducts(page, 20);
ctx.shared.pagination = {page, pageSize: 20, total};
return items;
}),
// declared after the routes of `products`: runs after each of them, and only them
pagination: mion.middleware(
(ctx): Pagination | void => ctx.shared.pagination
),
},
} satisfies Routes;
const api = mion.initRoutes(routes);
export type PaginationApi = typeof api;
middlewares.products.pagination.onResponse.