mion has an RPC style routing system so Routes are just functions that can be called remotely.
The first parameter of the remote method is always the Call Context. The rest of parameters are the remote parameters that get deserialized and validated before the route gets executed.
There are no URLs or Paths when defining a route, instead the API is defined using a plain JavaScript object, where every entry of the object is a route or middleFn. This way we simplify referencing remote methods when calling them from the client.
Internally a URL is generated for each route so these can be referenced using regular HTTP requests, but all this complexity is managed transparently by the client.
Routes can be defined using the route function by passing the Handler as first parameter and RouteOptions as second.
satisfies operator.
This guarantees routers are fully typed.import {Routes, route} from '@mionjs/router';
import {memoryStoreService as db} from './full-example.app.ts';
export const routes = {
sayHello: route((ctx, name1: string, name2: string): string => {
return `Hello ${name1} and ${name2}.`;
}),
getSomeData: route(async (ctx, id: string): Data | RpcError<'data-not-found'> => {
const data = await db.getData(id);
return data || new RpcError({publicMessage: 'Data not found', type: 'data-not-found'});
}),
} satisfies Routes;
mion provides query() and mutation() handler functions to explicitly declare the intent of each route. This distinction enables the client to automatically optimize how requests are sent over the wire.
query() — for read-only operations. The client automatically sends small payloads via GET with ?data=base64url.mutation() — for operations that modify data. The client always sends data in the body.route() — no explicit intent, data is sent in the body. Does not participate in cache semantics.import {RpcError} from '@mionjs/core';
import {Routes, route, query, mutation} from '@mionjs/router';
type User = {name: string; surname: string};
const routes = {
// query() for read-only operations
// client automatically uses GET with ?data=base64url for small payloads
getUser: query((ctx, id: number): User | RpcError<'user-not-found'> => {
return {name: 'John', surname: 'Doe'};
}),
// mutation() for operations that modify data
// client always uses POST with body
createUser: mutation((ctx, user: User): User => {
return user;
}),
// route() still works as before (no query/mutation intent, client uses POST)
sayHello: route((ctx, name: string): string => {
return `Hello ${name}`;
}),
} satisfies Routes;
query() and mutation() is recommended. It makes your code more readable and enables future optimizations like caching layers.By default, mion allows objects with extra properties to pass validation. You can enable strictTypes globally or per-route to reject objects with unknown properties. See the Validation page for details.
import {Routes, route, initRouter} from '@mionjs/router';
interface User {
name: string;
email: string;
age: number;
}
// Enable strictTypes globally: rejects objects with unknown/extra properties
await initRouter({strictTypes: true});
// Or enable strictTypes per-route
const routes = {
// this route rejects objects with extra properties
createUser: route((ctx, user: User): User => user, {strictTypes: true}),
// this route accepts objects with extra properties
updateUser: route((ctx, user: Partial<User>): Partial<User> => user, {strictTypes: false}),
} satisfies Routes;
We can register routes by passing the routes to initMionRouter or explicitly calling registerRoutes once router has been initialized.
import {HeadersSubset} from '@mionjs/core';
import {initMionRouter, Routes, CallContext, registerRoutes, route, headersFn} from '@mionjs/router';
export type Shared = () => Record<string, any>;
export type Context = CallContext<Shared>;
const authRoutes = {
logIn: route((c, email: string, password: string): string => 'loggedIn'),
logOut: route((): string => 'loggedOut'),
} satisfies Routes;
const routes = {
auth: headersFn((c: Context, h: HeadersSubset<'Authorization'>): void => undefined),
sayHello: route((c, name: string): string => 'hello' + name),
sayHello2: route((c, name: string): string => 'hello' + name),
} satisfies Routes;
export const mayApi = await initMionRouter(routes);
export const authApi = await registerRoutes(authRoutes);
// export api types to be consumed by the clients
export type MyApi = typeof mayApi;
export type AuthApi = typeof authApi;
Keep it simple and use regular valid JS variable names for routes. It is not recommended to use the array notation (using quotes) to define route names.
import {Routes, route} from '@mionjs/router';
const sayHello = route((ctx, name: string): string => {
return `Hello ${name}.`;
});
const routes = {
'say-Hello': sayHello, // path = /say-Hello !! NOT Recommended
'say Hello': sayHello, // path = /say%20Hello !! ROUTE WONT BE FOUND
} satisfies Routes;
The CallContext contains all the data related to the ongoing call.
Most of the data within the CallContext is marked as read-only, this is because it is not recommended to modify the context manually. It is still possible to modify it (the context is not a real Immutable JS object).
To share data between middleFns and routes use the shared object within the Call Context.
import {RpcError, HeadersSubset} from '@mionjs/core';
import {Routes, initMionRouter, headersFn, route} from '@mionjs/router';
import {getAuthUser, isAuthorized} from 'MyAuth';
const authorizationMiddleFn = headersFn(
async (context, {headers}: HeadersSubset<'Authorization', 'User-id'>): Promise<void | RpcError<'not-authorized'>> => {
const token = headers.Authorization;
const userId = headers['User-id'];
const me = await getAuthUser(token, userId);
if (!isAuthorized(me)) {
return new RpcError({publicMessage: 'user is not authorized', type: 'not-authorized'});
}
context.shared.myUser = me; // user is added to ctx to shared with other routes/middleFns
}
);
const sayMyName = route((context): string => {
return `hello ${context.shared.myUser.name}`;
});
const routes = {
authorizationMiddleFn,
sayMyName,
} satisfies Routes;
export const apiSpec = await initMionRouter(routes);
It is possible to define a contextDataFactory function used to initialize the context data object on every request.
This factory function will be called before any route or middleFn gets executed and the returned value will be the default shared object for all routes and middleFns.
import {initMionRouter, route} from '@mionjs/router';
import type {CallContext, Routes} from '@mionjs/router';
import type {User} from './full-example.app.ts';
interface ContextData {
myUser: User | null;
// ... other context data properties
}
const initContextData = (): ContextData => ({myUser: null});
type MyContext = CallContext<ContextData>;
const routes = {
getMyPet: route(async (ctx: MyContext): Promise<Pet> => {
const user = ctx.shared.myUser;
const pet = await myApp.db.getPetFromUser(user);
return pet;
}),
} satisfies Routes;
export const myApi = await initMionRouter(routes, {contextDataFactory: initContextData});