[routeResult, routeError, middleFnsResults, middleFnsErrors] - never throws, always returns.To use mion client we just need to initialize the client using the RemoteApi type returned from registerRoutes.
It is important to just import the type so we don't import any of the actual routes or backend code into the client.
import {initClient} from '@mionkit/client';
// importing only the RemoteApi type from server
import type {MyApi} from './server.routes.ts';
const {routes, middleFns} = initClient<MyApi>({baseURL: 'http://localhost:3000'});
The routes and middleFns objects returned from initClient contain all remote routes and middleFns.
If a middleFn is not public (does not expect any parameters and can't return data) then it is not included within the middleFns object.
Calling routes in the client generates a RouteSubRequest. We use the call() method to perform the remote call.
The call() method returns a 4-tuple [routeResult, routeError, middleFnsResults, middleFnsErrors] - it never throws.
import {initClient} from '@mionkit/client';
import type {MyApi} from './server.routes.ts';
const {routes} = initClient<MyApi>({baseURL: 'http://localhost:3000'});
async function callRoute() {
// calls sum route in the server
const [sum, error] = await routes.utils.sum(5, 2).call();
if (error) {
console.log('Error:', error.publicMessage);
} else {
console.log(sum); // 7
}
}
For routes that require middleFns, use callWithMiddleFns() to pass middleFn data alongside the route call:
import {initClient} from '@mionkit/client';
import type {MyApi} from './server.routes.ts';
import {HeadersSubset} from '@mionkit/core';
const {routes, middleFns} = initClient<MyApi>({baseURL: 'http://localhost:3000'});
// calls route with auth middleFn
// Returns 4-tuple: [routeResult, routeError, middleFnsResults, middleFnsErrors]
const [user, routeError, middleFnResults, middleFnErrors] = await routes.users.getById('123').callWithMiddleFns({
auth: middleFns.auth(new HeadersSubset({Authorization: 'myToken-XYZ'})),
});
if (routeError || middleFnErrors?.auth) {
console.log('Something failed');
} else {
console.log(user); // User object
}
For cases like authorization middleFns that are required for all requests, we can use the prefill() method of MiddleFnSubRequest. This will automatically prefill any future route calls that require that middleFn.
When using prefilled middleFns, middleFn results and errors are available in two ways:
RpcError typesimport {initClient} from '@mionkit/client';
import type {MyApi} from './server.routes.ts';
import {HeadersSubset} from '@mionkit/core';
const {routes, middleFns} = initClient<MyApi>({baseURL: 'http://localhost:3000'});
declare function redirectToLogin(): void;
declare function refreshToken(): Promise<string>;
// prefill() returns a TypedEvent for registering persistent handlers
// TypedEvent handlers are STRONGLY TYPED by the error.type string
middleFns
.auth(new HeadersSubset({Authorization: 'myToken-XYZ'}))
.prefill()
.onSuccess((session) => {
// Called after every successful auth
console.log('Authenticated as:', session?.userId);
})
.onError('not-authorized', (error) => {
// TypeScript knows error.type is 'invalid-token'
console.log('Auth failed:', error.publicMessage);
redirectToLogin();
});
// call() returns a 4-tuple with middleFnResults and middleFnErrors
// These are NOT strongly typed - they contain generic RpcError types
const [sum, error, middleFnResults, middleFnErrors] = await routes.utils.sum(5, 2).call();
// Both TypedEvent handlers AND 4-tuple receive the same middleFn data:
// - TypedEvent handlers were already called above (if auth succeeded/failed)
// - middleFnResults/middleFnErrors also contain the auth result/error
if (middleFnErrors?.auth) {
console.log('Auth error (generic type):', middleFnErrors.auth.publicMessage);
}
if (middleFnResults?.auth) {
console.log('Session from tuple:', middleFnResults.auth);
}
if (!error) {
console.log(sum); // 7
}
type string./* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/require-await */
import {initClient} from '@mionkit/client';
import {HeadersSubset} from '@mionkit/core';
// importing only the RemoteApi type from server
import type {MyApi} from './server.routes.ts';
const john = {id: '123', name: 'John', surname: 'Doe'};
const {routes, middleFns} = initClient<MyApi>({baseURL: 'http://localhost:3000'});
// ========== MiddleFn with Typed Success Return and Error Handling ==========
// prefills auth token for any future requests, value is stored in localStorage by default
// Returns TypedEvent for registering persistent success and error handlers
// The auth middleFn returns SessionInfo on success (when returnSession=true) or RpcError<'not-authorized', NotAuthorizedData>
const authHeaders = new HeadersSubset({Authorization: 'Bearer myToken-XYZ'});
middleFns
.auth(authHeaders, true) // returnSession=true to get SessionInfo back
.prefill()
// onSuccess receives the strongly typed SessionInfo (or void when returnSession=false)
.onSuccess((sessionInfo) => {
// Since we passed returnSession=true, we know sessionInfo is SessionInfo
// TypeScript infers: sessionInfo is SessionInfo | void, so we narrow it
if (!sessionInfo) return;
// Now TypeScript knows sessionInfo is SessionInfo!
// sessionInfo.role is 'admin' | 'user' | 'guest'
console.log('Logged in as:', sessionInfo.userId);
console.log('Role:', sessionInfo.role);
// Use session info to configure app state
if (sessionInfo.role === 'admin') {
console.log('Admin features enabled');
}
})
.onError('not-authorized', (error) => {
// error.errorData is strongly typed as NotAuthorizedData!
// TypeScript knows: error.errorData?.reason is 'missing-token' | 'invalid-token' | 'expired-token'
const reason = error.errorData?.reason;
if (reason === 'expired-token') {
console.log('Token expired, refreshing...');
} else if (reason === 'missing-token') {
console.log('No token provided, redirecting to login...');
} else {
console.log('Invalid token:', error.publicMessage);
}
})
.onError('validation-error', (error) => {
console.log('Validation error:', error.errorData?.typeErrors);
});
// ========== Example 1: Route with strongly-typed errorData ==========
// getById returns User | RpcError<'user-not-found', UserNotFoundData>
// call() returns 4-tuple: [routeResult, routeError, middleFnsResults, middleFnsErrors]
async function exampleWithTypedError() {
const [user, error] = await routes.users.getById('USER-123').call();
if (error && error.type === 'user-not-found') {
// error.errorData is strongly typed as UserNotFoundData!
console.log('User not found. Requested ID:', error.errorData?.requestedId);
if (error.errorData?.suggestedIds?.length) {
console.log('Did you mean one of these?', error.errorData.suggestedIds.join(', '));
}
return;
} else if (error) {
// Catches any other errors (network errors, middleFn errors, etc.)
console.log('Unexpected error:', error.publicMessage);
return;
}
// After error check, user is guaranteed to be User here
// Use optional chaining for TypeScript strictness
console.log('Found user:', user?.name, user?.surname);
}
// ========== Example 2: Order error with typed errorData ==========
// Order getById returns Order | RpcError<'order-not-found', OrderNotFoundData>
async function exampleWithOrderError() {
const [order, error] = await routes.orders.getById('ORDER-404').call();
if (error && error.type === 'order-not-found') {
// error.errorData is strongly typed as OrderNotFoundData!
console.log('Order not found. Requested ID:', error.errorData?.requestedId);
return;
}
// After error check, order is guaranteed to be Order here
console.log('Order total:', order?.totalUSD);
}
// ========== Example 3: Route that always succeeds ==========
// sayHello returns just string (no error type), so error is always undefined
async function exampleAlwaysSucceeds() {
const [result, error] = await routes.users.sayHello(john).call();
// sayHello never has an error type, so we can use the result directly
console.log(result); // Hello John Doe
}
// ========== Example 5: Using callWithMiddleFns() for per-request middleFns ==========
// Use callWithMiddleFns() when you need to pass middleFns for a SINGLE request
// Returns 4-tuple: [routeResult, routeError, middleFnsResults, middleFnsErrors]
// Create a middleFn with temporary credentials for this specific request
const tempAuthHeaders: HeadersSubset<'Authorization'> = {headers: {Authorization: 'Bearer temp-token-ABC'}};
// callWithMiddleFns() takes a record of middleFns and returns a typed 4-tuple
async function exampleWithCallWithMiddleFns() {
const [user, routeError, middleFnResults, middleFnErrors] = await routes.users.getById('USER-123').callWithMiddleFns({
auth: middleFns.auth(tempAuthHeaders, true),
});
// Check for route errors
if (routeError?.type === 'user-not-found') {
console.log('User not found:', routeError.errorData?.requestedId);
}
// Check middleFn errors
if (middleFnErrors?.auth?.type === 'not-authorized') {
const authError = middleFnErrors.auth;
const reason = authError.errorData?.reason;
if (reason === 'expired-token') {
console.log('Temp token expired, requesting new one...');
}
}
// Access success data
if (user) console.log('Found user:', user.name);
if (middleFnResults?.auth) console.log('Authenticated as:', middleFnResults.auth.userId);
}
// ========== Example 6: Multiple MiddleFns with callWithMiddleFns() ==========
// Pass multiple middleFns in the record - each gets its own typed result
async function exampleWithMultipleMiddleFns() {
const [user, routeError, middleFnResults, middleFnErrors] = await routes.users.getById('USER-123').callWithMiddleFns({
auth: middleFns.auth(tempAuthHeaders),
// session: middleFns.session('session-token'), // If you have a session middleFn
});
// Handle each middleFn's errors independently
if (middleFnErrors?.auth) {
console.log('Auth failed:', middleFnErrors.auth.publicMessage);
}
// Access success data
if (user) console.log('User:', user.name);
}
// ========== Example 7: Using call() with async/await (recommended) ==========
// call() returns 4-tuple: [routeResult, routeError, middleFnsResults, middleFnsErrors]
// This is the standard pattern for all route calls
async function exampleWithCall() {
// call() never throws - returns a 4-tuple
// Partial destructuring still works for backward compatibility
const [user, error] = await routes.users.getById('USER-999').call();
if (error) {
// TypeScript knows error is the typed error here
// Each error type can be checked
if (error.type === 'user-not-found') {
// error.errorData is still strongly typed!
console.log('User not found:', error.errorData?.requestedId);
} else {
console.log('Other error:', error.publicMessage);
}
return;
}
// After error check, user is guaranteed to be User here
// Use optional chaining for TypeScript strictness
console.log('User:', user?.name, user?.surname);
// validate parameters locally without calling the server
const validationResp = await routes.users.sayHello(john).typeErrors();
console.log(validationResp); // []
}
import {RpcError, HeadersSubset} from '@mionkit/core';
import {Routes, headersFn, middleFn, initMionRouter, route} from '@mionkit/router';
import {Logger} from 'Logger';
export type User = {id: string; name: string; surname: string};
export type Order = {id: string; date: Date; userId: string; totalUSD: number};
// Session info returned by auth middleFn - strongly typed in client onSuccess!
export type SessionInfo = {
userId: string;
role: 'admin' | 'user' | 'guest';
permissions: string[];
expiresAt: Date;
};
// Error data types - these will be strongly typed in the client!
export type UserNotFoundData = {requestedId: string; suggestedIds?: string[]};
export type OrderNotFoundData = {requestedId: string};
export type NotAuthorizedData = {reason: 'missing-token' | 'invalid-token' | 'expired-token'};
// Simulated database
const usersDb: Record<string, User> = {
'USER-123': {id: 'USER-123', name: 'John', surname: 'Smith'},
};
const routes = {
// MiddleFn with typed errorData and typed success return
// When returnSession is true, returns SessionInfo - strongly typed in client onSuccess!
auth: headersFn(
(
ctx,
h: HeadersSubset<'Authorization'>,
returnSession = false
): SessionInfo | void | RpcError<'not-authorized', NotAuthorizedData> => {
const token = h.headers.Authorization;
if (!token) {
throw new RpcError({
publicMessage: 'Not Authorized',
type: 'not-authorized',
errorData: {reason: 'missing-token'},
});
}
// Return session info if requested
if (returnSession) {
return {
userId: 'USER-123',
role: 'admin',
permissions: ['read', 'write', 'delete'],
expiresAt: new Date(Date.now() + 3600000), // 1 hour from now
};
}
}
),
users: {
// Route with typed errorData - client can access error.errorData.requestedId
getById: route((ctx, id: string): User | RpcError<'user-not-found', UserNotFoundData> => {
const user = usersDb[id];
if (!user) {
return new RpcError({
publicMessage: `User ${id} not found`,
type: 'user-not-found',
errorData: {requestedId: id, suggestedIds: ['USER-123']},
});
}
return user;
}),
delete: route((ctx, id: string): string => id),
create: route((ctx, user: Omit<User, 'id'>): User => ({id: 'USER-123', ...user})),
sayHello: route((ctx, user: User): string => `Hello ${user.name} ${user.surname}`),
},
orders: {
// Route with typed errorData
getById: route((ctx, id: string): Order | RpcError<'order-not-found', OrderNotFoundData> => {
if (id === 'ORDER-404') {
return new RpcError({
publicMessage: `Order ${id} not found`,
type: 'order-not-found',
errorData: {requestedId: id},
});
}
return {id, date: new Date(), userId: 'USER-123', totalUSD: 120};
}),
delete: route((ctx, id: string): string => id),
create: route((ctx, order: Omit<Order, 'id'>): Order => ({id: 'ORDER-123', ...order})),
},
utils: {
sum: route((ctx, a: number, b: number): number => a + b),
},
log: middleFn((ctx): void => Logger.log(ctx.path, ctx.request.headers, ctx.request.body), {runOnError: true}),
} satisfies Routes;
// init & register routes (this automatically registers client routes)
const myApi = await initMionRouter(routes);
// Export the type of the Api (used by the client)
export type MyApi = typeof myApi;
The Result type is a 4-tuple used by call() and callWithMiddleFns() to return route and middleFn results without throwing.
// Error processing code-import:
// Start comment marker not found: // type-result-start
The result type returned by callWithMiddleFns(). Returns a 4-tuple [routeResult, routeError, middleFnsResults, middleFnsErrors].
// Error processing code-import:
// Start comment marker not found: // type-call-with-middleFns-result-start
This type could be considered your API schema to be used by the client. This is a Mapping of your public middleFns and routes to PublicMethod.
/** Same as Public Api but no type mapping, should be easier to use than PublicApi when non strong types are required. */
export type RemoteApi = {
[key: string]: PublicRoute | PublicMiddleFn | PublicHeadersFn | RemoteApi;
};
// Error processing code-import:
// Start comment marker not found: // type-sub-request-start
// Error processing code-import:
// Start comment marker not found: // type-route-sub-request-start
// Error processing code-import:
// Start comment marker not found: // type-middleFn-sub-request-start