A routesFlow is a client-orchestrated query that executes multiple routes in a single HTTP request. You can batch independent calls together and map results between them using mapFrom — resolving relationships server-side without extra round trips.
routesFlow and mapFrom.
Use the routesFlow() function to execute multiple routes together:
import {initClient, routesFlow} from '@mionkit/client';
import type {MyApi} from './server.routes.ts';
const {routes, middleFns} = initClient<MyApi>({baseURL: 'http://localhost:3000'});
async function basicRoutesFlow() {
// Execute multiple routes in a single HTTP request
const [[sum, user], [sumError, userError]] = await routesFlow([routes.utils.sum(5, 2), routes.users.getById('USER-123')]);
// Results are returned as arrays in the same order as the routes
if (sumError) console.log('Sum error:', sumError.publicMessage);
else console.log('Sum:', sum); // 7
if (userError) console.log('User error:', userError.publicMessage);
else console.log('User:', user); // {id: 'USER-123', name: 'John', surname: 'Smith'}
}
mapFromUse mapFrom() to map the output of one route to the input of another — all resolved server-side in a single HTTP request. This enables GraphQL-like data composition with the simplicity and type safety of RPC.
import {initClient, routesFlow, mapFrom} from '@mionkit/client';
import type {MyApi} from './server.routes.ts';
const {routes, middleFns} = initClient<MyApi>({baseURL: 'http://localhost:3000'});
// Fetch an order
const orderReq = routes.orders.getById('ORDER-123');
// mapFrom maps order.userId → getById input, (runs server-side)
const mapping = mapFrom(orderReq, (order) => order!.userId);
// fake is just used for faking the expected type of users.getById in the client
const userReq = routes.users.getById(mapping.type());
const [[orderData, userData]] = await routesFlow([orderReq, userReq]);
if (orderData && userData) {
console.log(`Order ${orderData.id} placed by ${userData.name} ${userData.surname}`);
}
mapFrom(sourceRoute, mapperFn) creates a reference to the source route's output.fake() provides a type-safe placeholder so TypeScript can infer the result typesThe mapper function passed to mapFrom must be a pure function — no closures, no external variables, no side effects. This is because the function body is extracted at compile time by the mion vite plugin and bundled into the server. The server executes the function in its own context, so it has no access to your client-side scope.
// ✅ Pure function — only uses the input parameter
mapFrom(order, (o) => o!.userId)
// ✅ Pure function — inline computation is fine
mapFrom(order, (o) => o!.totalUSD * 100)
// ❌ NOT pure — references external variable
const multiplier = 2;
mapFrom(order, (o) => o!.totalUSD * multiplier)
To enable mapFrom, the server vite config must include the serverPureFunctions option pointing to your client source directory. This tells the plugin where to scan for mapper functions to bundle into the server:
// server vite.config.ts
import {mionPlugin} from '@mionkit/devtools/vite-plugin';
import {resolve} from 'path';
export default defineConfig({
plugins: [
mionPlugin({
serverPureFunctions: {
clientSrcPath: resolve(__dirname, '../client/src'),
},
}),
],
});
| Option | Description |
|---|---|
clientSrcPath | Path to the client source directory containing mapFrom() calls |
include | Glob patterns for files to scan (defaults to ['**/*.ts', '**/*.tsx']) |
exclude | Glob patterns for files to exclude from scanning |
The key difference between call() and routesFlow() is in the response structure:
| Aspect | route.call() | routesFlow([routes...]) |
|---|---|---|
| HTTP Requests | One per route | One for all routes |
| Result | Single value | Array of values |
| Error | Single error | Array of errors |
| Position | Direct access | Index-based access |
import {initClient, routesFlow} from '@mionkit/client';
import type {MyApi} from './server.routes.ts';
const {routes} = initClient<MyApi>({baseURL: 'http://localhost:3000'});
// ============================================
// SINGLE ROUTE CALL - call()
// ============================================
// Result and error are the direct types from the route
async function singleRouteCall() {
// Returns: [result, error, middleFnResults, middleFnErrors]
const [user, error] = await routes.users.getById('USER-123').call();
// `user` is User | undefined
// `error` is RpcError<'user-not-found', UserNotFoundData> | ValidationError | undefined
if (error) {
if (error.type === 'user-not-found') {
console.log('User not found:', error.errorData?.requestedId);
}
} else {
console.log('User:', user?.name);
}
}
// ============================================
// ROUTES_FLOW - Multiple routes in one request
// ============================================
// Results and errors are ARRAYS in the same order as the routes
async function routesFlowCall() {
// Returns: [[results...], [errors...], middleFnResults, middleFnErrors]
const [[user, order], [userError, orderError]] = await routesFlow([
routes.users.getById('USER-123'),
routes.orders.getById('ORDER-1'),
]);
// `user` is User | undefined (first route result)
// `order` is Order | undefined (second route result)
// `userError` is RpcError<'user-not-found'> | ValidationError | undefined
// `orderError` is RpcError<'order-not-found'> | ValidationError | undefined
// Each result/error corresponds to its route by position
if (userError) console.log('User error:', userError.publicMessage);
else console.log('User:', user?.name);
if (orderError) console.log('Order error:', orderError.publicMessage);
else console.log('Order:', order?.id);
}
// Single route call - direct result/error
const [result, error, middleFnResults, middleFnErrors] = await route.call();
// RoutesFlow - arrays of results/errors
const [[result1, result2], [error1, error2], middleFnResults, middleFnErrors] = await routesFlow([route1, route2]);
You can include middleFns (like authentication) in your routesFlow:
import {initClient, routesFlow} from '@mionkit/client';
import {HeadersSubset} from '@mionkit/core';
import type {MyApi} from './server.routes.ts';
const {routes, middleFns} = initClient<MyApi>({baseURL: 'http://localhost:3000'});
async function routesFlowWithMiddleFns() {
const authHeaders = new HeadersSubset({Authorization: 'my-token'});
// Execute routesFlow with explicit middleFns
const [[sum, user], [sumError, userError], middleFnResults, middleFnErrors] = await routesFlow(
[routes.utils.sum(5, 2), routes.users.getById('USER-123')],
{auth: middleFns.auth(authHeaders)}
);
// Check middleFn errors
if (middleFnErrors?.auth) {
console.log('Auth failed:', middleFnErrors.auth.publicMessage);
return;
}
// Handle route results
if (!sumError) console.log('Sum:', sum);
if (!userError) console.log('User:', user);
}
If you prefer starting from a route, use callWithWorkflow() to add more routes:
import {initClient} from '@mionkit/client';
import {HeadersSubset} from '@mionkit/core';
import type {MyApi} from './server.routes.ts';
const {routes, middleFns} = initClient<MyApi>({baseURL: 'http://localhost:3000'});
async function callWithWorkflowExample() {
const authHeaders = new HeadersSubset({Authorization: 'my-token'});
// Alternative syntax: start from a route and add more routes to the routesFlow
const [[sum, user, order], [sumError, userError, orderError]] = await routes.utils
.sum(5, 2)
.callWithWorkflow([routes.users.getById('USER-123'), routes.orders.getById('ORDER-1')], {
auth: middleFns.auth(authHeaders),
});
// Handle results - same array pattern as routesFlow()
if (!sumError) console.log('Sum:', sum);
if (!userError) console.log('User:', user);
if (!orderError) console.log('Order:', order);
}
Both routesFlow() and callWithWorkflow() produce the same result - choose the syntax that fits your code style.
In a routesFlow, each route can succeed or fail independently. The error array contains errors at the same index as their corresponding routes:
const [[user, order, sum], [userError, orderError, sumError]] = await routesFlow([
routes.users.getById('USER-123'), // index 0
routes.orders.getById('ORDER-404'), // index 1 - this will fail
routes.utils.sum(5, 2), // index 2
]);
// Results:
// user = {id: 'USER-123', ...} - success
// userError = undefined
// order = undefined - failed
// orderError = RpcError<'order-not-found'>
// sum = 7 - success
// sumError = undefined
Complex types like Date, Map, and Set are automatically serialized and deserialized in routesFlows, just like single route calls:
const testDate = new Date('2024-06-15T12:30:00.000Z');
const testMap = new Map([['a', 1], ['b', 2]]);
const [[date, map], [dateError, mapError]] = await routesFlow([
routes.getSameDate(testDate),
routes.getSameMap(testMap),
]);
// date is a Date instance
// map is a Map instance
// ❌ This will throw an error
const {routes: routes1} = initClient<MyApi>({baseURL: 'http://server1.com'});
const {routes: routes2} = initClient<MyApi>({baseURL: 'http://server2.com'});
await routesFlow([routes1.getUser('1'), routes2.getOrder('1')]); // Error!
// ✅ Use routes from the same client
const {routes} = initClient<MyApi>({baseURL: 'http://localhost:3000'});
await routesFlow([routes.getUser('1'), routes.getOrder('1')]); // Works!