Client Middleware
Every server middleware with params or a result shows up in the client's middlewares object. Use it to send the middleware its data and handle what it returns.
Middleware Hooks
Each middleware in middlewares has three hooks. onRequest sends its data, like a trace id. onResponse gets its result and onError its declared errors.
import {HeadersSubset} from '@mionjs/core';
import {initClient} from '@mionjs/client';
import type {MyApi} from './middleware-hooks.routes.ts';
const {middlewares} = initClient<MyApi>({
baseURL: 'http://localhost:3000',
});
declare function redirectToLogin(): void;
// a fresh id for every request, the server echoes it back
middlewares.trace
.onRequest((trace) => {
// the remote middleware call: same params as the server's traceHandler
trace(new HeadersSubset({'X-Trace-Id': crypto.randomUUID()}));
})
.onResponse((info) => {
console.log('Trace:', info.traceId, 'received at', info.receivedAt);
});
// auth takes no params, so it needs no onRequest
middlewares.auth
.onResponse((session) => {
console.log('Authenticated as:', session.userId);
})
.onError('not-authorized', (error) => {
console.log('Auth failed:', error.errorData?.reason);
redirectToLogin();
});
import {HeadersSubset, FatalError} from '@mionjs/core';
import {createMionRouter, Routes} from '@mionjs/router';
import type {CallContext} from '@mionjs/router';
import {getSession, type SessionInfo} from './session.ts';
const mion = createMionRouter();
export type TraceInfo = {traceId: string; receivedAt: Date};
export type NotAuthorizedData = {reason: 'no-session' | 'expired-session'};
// a plain handler, so a client installer can take its type
export function traceHandler(
ctx: CallContext,
{headers}: HeadersSubset<'X-Trace-Id'>
): TraceInfo {
return {traceId: headers['X-Trace-Id'], receivedAt: new Date()};
}
// no params: it reads the HttpOnly session cookie the browser sends
export function authHandler(
ctx: CallContext
): SessionInfo | FatalError<'not-authorized', NotAuthorizedData> {
const session = getSession(ctx.request.headers.get('cookie'));
// a returned FatalError ends the request (no route runs) and stays typed
if (!session) {
return new FatalError({
publicMessage: 'Not Authorized',
type: 'not-authorized',
errorData: {reason: 'no-session'},
});
}
return session;
}
const routes = {
trace: mion.headersMiddleware(traceHandler),
auth: mion.middleware(authHandler),
utils: {
sum: mion.route((ctx, a: number, b: number): number => a + b),
},
} satisfies Routes;
const myApi = mion.initRoutes(routes);
export type MyApi = typeof myApi;
| Hook | Runs | Removed with |
|---|---|---|
onRequest | Before each request that runs the middleware, a route's first call included. It can be async. Its second argument is the request, to pick data per route. A middleware has one. | offRequest() |
onResponse | After each successful middleware result, strongly typed. Several run in the order added, even when one throws. | offResponse(handler), only that one |
onError | After each declared error of the given type, strongly typed. Several per type run in the order added, even when one throws. | offError(type, handler), only that one |
onRequest gets a function. Call it with the middleware params, or skip it to send nothing.onRequest fails, the request is not sent and the error goes to response['@thrownErrors']. onError only gets the errors the middleware declares.Reading Middleware Results From Routes
Every call returns a third value, response, with each middleware's answer. It is typed with the middleware's return value, declared errors and ValidationError.
import {isRpcError} from '@mionjs/core';
import {initClient} from '@mionjs/client';
import type {MyApi} from './middleware-results.routes.ts';
const {routes} = initClient<MyApi>({baseURL: 'http://localhost:3000'});
const [sum, error, response] = await routes.utils.sum(5, 2).call();
// the auth middleware's answer or declared error, at its path
if (isRpcError(response.auth))
console.log('Auth error:', response.auth.publicMessage);
else console.log('Session:', response.auth?.userId);
if (!error) console.log(sum); // 7
import {FatalError} from '@mionjs/core';
import {createMionRouter, Routes} from '@mionjs/router';
import {getSession, type SessionInfo} from './session.ts';
const mion = createMionRouter();
const routes = {
auth: mion.middleware(
(ctx): SessionInfo | FatalError<'not-authorized'> =>
getSession(ctx.request.headers.get('cookie')) ??
new FatalError({type: 'not-authorized', publicMessage: 'Not Authorized'})
),
utils: {
sum: mion.route((ctx, a: number, b: number): number => a + b),
},
} satisfies Routes;
const myApi = mion.initRoutes(routes);
export type MyApi = typeof myApi;
Retrying a Call
onResponse and onError get the call as a second argument. Its retry() sends the whole call again, for example after refreshing the session, and runs every onRequest first. Each middleware can retry once per call.
| Route | Retry allowed |
|---|---|
query() | Always |
mutation() or route() | Only if the call failed: the route returned an error, or it returned nothing and the response has an error |
retry() returns false when it is not allowed, and the call keeps its current result.Reusable Client Middleware
A middleware is usually written only for the server. With mion it can also ship its client side as an installer, a function that receives middlewares.x and sets its hooks, so every app gets the same behaviour. Type its parameter with ClientMiddlewareOf<typeof handler>, so it works wherever the app places the middleware.
import {HeadersSubset} from '@mionjs/core';
import {initClient} from '@mionjs/client';
import type {ClientMiddlewareOf} from '@mionjs/client';
import type {MyApi, authHandler} from './middleware-hooks.routes.ts';
// only the handler's type crosses over, so this file never loads the router
export function useAuth(
auth: ClientMiddlewareOf<typeof authHandler>,
refreshSession: () => Promise<boolean>
) {
auth.onError('not-authorized', async (error, context) => {
// the server sets a new session cookie, then the whole call is sent again
if (await refreshSession()) context.retry();
});
}
declare function refreshSession(): Promise<boolean>;
const {routes, middlewares} = initClient<MyApi>({
baseURL: 'http://localhost:3000',
});
useAuth(middlewares.auth, refreshSession);
middlewares.trace.onRequest((trace) =>
trace(new HeadersSubset({'X-Trace-Id': crypto.randomUUID()}))
);
const [sum] = await routes.utils.sum(5, 2).call();
rpc-client-middleware-not-set-up when a called route runs a middleware with required params and the client never sets it up.