mion client uses a Result Pattern for responses, that returns a tuple with 4 elements:
[routeResult, routeError, middleFnsResults, middleFnsErrors]
0: Route result (if successful)1: Route error (if failed)2: MiddleFn results (by middleFn name)3: MiddleFn errors (by middleFn name)The API calls never throw, this provides type-safe error handling with full TypeScript support!
import {initClient} from '@mionkit/client';
import type {MyApi} from './server.routes.ts';
const {routes, middleFns} = initClient<MyApi>({baseURL: 'http://localhost:3000'});
async function basicErrorHandling() {
// call() returns 4-tuple - never throws
const [user, error] = await routes.users.getById('123').call();
if (error) {
// Handle error - TypeScript knows the error type
console.log('Error:', error.publicMessage);
} else {
// Handle success
console.log('User:', user?.name);
}
}
When routes define specific error types, TypeScript provides full autocompletion and type checking:
import {initClient} from '@mionkit/client';
// importing type only from server
import type {MyApi} from './server.routes.ts';
import {isRpcError} from '@mionkit/core';
const {routes, middleFns} = initClient<MyApi>({baseURL: 'http://localhost:3000'});
// ========== Result pattern (never throws) ==========
// call() and callWithMiddleFns() always return a 4-tuple, never throw
// [routeResult, routeError, middleFnsResults, middleFnsErrors]
// calls sayHello route in the server
const [sayHello, error] = await routes.users.sayHello({id: '123', name: 'John', surname: 'Doe'}).call();
if (error) {
// in this case the request has failed because the authorization middleFn is missing
console.log(error); // { type: 'validation-error', message: `Invalid params for Route or MiddleFn 'auth'.`}
if (isRpcError(error)) {
// ... handle the error as required
}
} else {
console.log(sayHello); // Hello John Doe
}
// ========== Full 4-tuple with middleFns ==========
// callWithMiddleFns() returns [routeResult, routeError, middleFnsResults, middleFnsErrors]
const [greeting, routeError, middleFnResults, middleFnErrors] = await routes.users
.sayHello({id: '123', name: 'John', surname: 'Doe'})
.callWithMiddleFns({
auth: middleFns.auth({headers: {Authorization: 'Bearer token'}}),
});
if (routeError) {
console.log('Route failed:', routeError.type);
} else {
console.log(greeting); // Hello John Doe
}
// Check middleFn errors
if (middleFnErrors?.auth) {
console.log('Auth middleFn failed:', middleFnErrors.auth.type);
}
// Access middleFn results
console.log('MiddleFn results:', middleFnResults);
// ========== Validation throws errors ==========
// Note: typeErrors() is the only method that can throw
try {
// Validation throws an error when validation fails
const errors = await routes.users.sayHello(null as any).typeErrors();
console.log(errors); // [] (empty array if no errors)
} catch (validationError: any) {
console.log(validationError); // { type: 'validation-error', message: `Invalid params ...`, errorData : {...}}
}
import {RpcError, HeadersSubset} from '@mionkit/core';
import {headersFn, rawMiddleFn, middleFn, Routes, initMionRouter, route} from '@mionkit/router';
import {Context, NewUser, getSharedData, myApp} from './full-example.app.ts';
import {User} from './full-example.app.ts';
const getUser = route((ctx: Context, id: number): User | RpcError<'user-not-found'> => {
const user = myApp.store.getUser(id);
if (!user) return new RpcError({publicMessage: 'User not found', type: 'user-not-found'});
return user;
});
const createUser = route((ctx: Context, newUser: NewUser): User => myApp.store.createUser(newUser));
const updateUser = route((ctx: Context, user: User): User | RpcError<'user-not-found'> => {
const updated = myApp.store.updateUser(user);
if (!updated) return new RpcError({publicMessage: 'User not found, can not be updated', type: 'user-not-found'});
return updated;
});
const deleteUser = route((ctx: Context, id: number): User | RpcError<'user-not-found'> => {
const deleted = myApp.store.deleteUser(id);
if (!deleted) return new RpcError({publicMessage: 'User not found, can not be deleted', type: 'user-not-found'});
return deleted;
});
const auth = headersFn((ctx: Context, {headers}: HeadersSubset<'Authorization'>): void => {
const token = headers.Authorization;
if (!myApp.auth.isAuthorized(token))
throw new RpcError({
publicMessage: 'Not Authorized',
type: 'not-authorized',
});
ctx.shared.me = myApp.auth.getIdentity(token) as User;
});
const log = rawMiddleFn((context: Context): void => console.log('rawMiddleFn', context.path));
const routes = {
private: middleFn((): null => null),
auth,
users: {
get: getUser, // api/v1/users/get
create: createUser, // api/v1/users/create
update: updateUser, // api/v1/users/update
delete: deleteUser, // api/v1/users/delete
},
log,
} satisfies Routes;
export const apiSpec = await initMionRouter(routes, {
contextDataFactory: getSharedData,
prefix: 'api/v1',
});
export type ApiSpec = typeof apiSpec;
When you need to pass middleFn data to a single route call, use callWithMiddleFns(). This method returns a tuple that includes both route and middleFn results.
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
}
import {RpcError, HeadersSubset} from '@mionkit/core';
import {headersFn, rawMiddleFn, middleFn, Routes, initMionRouter, route} from '@mionkit/router';
import {Context, NewUser, getSharedData, myApp} from './full-example.app.ts';
import {User} from './full-example.app.ts';
const getUser = route((ctx: Context, id: number): User | RpcError<'user-not-found'> => {
const user = myApp.store.getUser(id);
if (!user) return new RpcError({publicMessage: 'User not found', type: 'user-not-found'});
return user;
});
const createUser = route((ctx: Context, newUser: NewUser): User => myApp.store.createUser(newUser));
const updateUser = route((ctx: Context, user: User): User | RpcError<'user-not-found'> => {
const updated = myApp.store.updateUser(user);
if (!updated) return new RpcError({publicMessage: 'User not found, can not be updated', type: 'user-not-found'});
return updated;
});
const deleteUser = route((ctx: Context, id: number): User | RpcError<'user-not-found'> => {
const deleted = myApp.store.deleteUser(id);
if (!deleted) return new RpcError({publicMessage: 'User not found, can not be deleted', type: 'user-not-found'});
return deleted;
});
const auth = headersFn((ctx: Context, {headers}: HeadersSubset<'Authorization'>): void => {
const token = headers.Authorization;
if (!myApp.auth.isAuthorized(token))
throw new RpcError({
publicMessage: 'Not Authorized',
type: 'not-authorized',
});
ctx.shared.me = myApp.auth.getIdentity(token) as User;
});
const log = rawMiddleFn((context: Context): void => console.log('rawMiddleFn', context.path));
const routes = {
private: middleFn((): null => null),
auth,
users: {
get: getUser, // api/v1/users/get
create: createUser, // api/v1/users/create
update: updateUser, // api/v1/users/update
delete: deleteUser, // api/v1/users/delete
},
log,
} satisfies Routes;
export const apiSpec = await initMionRouter(routes, {
contextDataFactory: getSharedData,
prefix: 'api/v1',
});
export type ApiSpec = typeof apiSpec;
For middleFns that should be included in all requests (like authentication), use prefill():
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'});
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
}
prefill().onError().onSuccess() handlers are called on every request regardless if they are handled in the route call response or not.When you prefill a middleFn, you can register persistent success/error handlers using TypedEvent:
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'});
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
}
| Aspect | .prefill() | .callWithMiddleFns() |
|---|---|---|
| Persistence | MiddleFn data stored for ALL future requests | MiddleFn data sent with this request only |
| Error Handlers | Persistent via .onError() | Check middleFnErrors in tuple |
| Use Case | Auth tokens, session data | Metadata related to the request |
import {initClient} from '@mionkit/client';
// importing only the RemoteApi type from server
import type {MyApi} from './server.routes.ts';
import {HeadersSubset} from '@mionkit/core';
const john = {id: '123', name: 'John', surname: 'Doe'};
const {routes, middleFns} = initClient<MyApi>({baseURL: 'http://localhost:3000'});
async function fullExample() {
// prefills auth token for any future requests, value is stored in localStorage by default
await middleFns.auth(new HeadersSubset({Authorization: 'myToken-XYZ'})).prefill();
// calls sayHello route in the server - call() returns [result, error] tuple
const [greeting, error] = await routes.users.sayHello(john).call();
if (error) {
console.log('Error:', error.publicMessage);
} else {
console.log(greeting); // Hello John Doe
}
// validate parameters locally without calling the server
const validationErrors = await routes.users.sayHello(john).typeErrors();
console.log(validationErrors); // [] (empty array if no errors)
}
import {RpcError, HeadersSubset} from '@mionkit/core';
import {headersFn, rawMiddleFn, middleFn, Routes, initMionRouter, route} from '@mionkit/router';
import {Context, NewUser, getSharedData, myApp} from './full-example.app.ts';
import {User} from './full-example.app.ts';
const getUser = route((ctx: Context, id: number): User | RpcError<'user-not-found'> => {
const user = myApp.store.getUser(id);
if (!user) return new RpcError({publicMessage: 'User not found', type: 'user-not-found'});
return user;
});
const createUser = route((ctx: Context, newUser: NewUser): User => myApp.store.createUser(newUser));
const updateUser = route((ctx: Context, user: User): User | RpcError<'user-not-found'> => {
const updated = myApp.store.updateUser(user);
if (!updated) return new RpcError({publicMessage: 'User not found, can not be updated', type: 'user-not-found'});
return updated;
});
const deleteUser = route((ctx: Context, id: number): User | RpcError<'user-not-found'> => {
const deleted = myApp.store.deleteUser(id);
if (!deleted) return new RpcError({publicMessage: 'User not found, can not be deleted', type: 'user-not-found'});
return deleted;
});
const auth = headersFn((ctx: Context, {headers}: HeadersSubset<'Authorization'>): void => {
const token = headers.Authorization;
if (!myApp.auth.isAuthorized(token))
throw new RpcError({
publicMessage: 'Not Authorized',
type: 'not-authorized',
});
ctx.shared.me = myApp.auth.getIdentity(token) as User;
});
const log = rawMiddleFn((context: Context): void => console.log('rawMiddleFn', context.path));
const routes = {
private: middleFn((): null => null),
auth,
users: {
get: getUser, // api/v1/users/get
create: createUser, // api/v1/users/create
update: updateUser, // api/v1/users/update
delete: deleteUser, // api/v1/users/delete
},
log,
} satisfies Routes;
export const apiSpec = await initMionRouter(routes, {
contextDataFactory: getSharedData,
prefix: 'api/v1',
});
export type ApiSpec = typeof apiSpec;
// Error processing code-import:
// Start comment marker not found: // type-result-start
// Error processing code-import:
// Start comment marker not found: // type-call-with-middleFns-result-start
// Error processing code-import:
// Start comment marker not found: // type-typed-event-start
export class RpcError<ErrType extends string, ErrData = any>
extends TypedError<ErrType>
implements RpcErrorParams<ErrType, ErrData>
{
// Note: name is NOT declared as a property here
// It is inherited from Error class and assigned in constructor
// This prevents it from being included in type reflection for JIT validation
/**
* id of the error, ideally each error should unique identifiable
* * if RouterOptions.autoGenerateErrorId is set to true and id with timestamp+uuid will be generated
* */
public readonly id?: number | string;
/** the message that will be returned in the response */
public readonly publicMessage: string;
/** options data related to the error, ie validation data, must be json serializable */
public readonly errorData?: Readonly<ErrData>;
/** optional http status code */
statusCode?: number;
constructor({message, publicMessage, originalError, errorData, type, id, statusCode}: AnyErrorParams<ErrType, ErrData>) {
const originalMessage = message || originalError?.message || publicMessage || '';
// Call parent TypedError constructor
super({
message: originalMessage,
originalError,
type,
});
const {autoGenerateErrorId} = options;
this.id = id ?? (autoGenerateErrorId ? randomUUID_V7() : undefined);
this.publicMessage = publicMessage || '';
this.errorData = errorData;
this.statusCode = statusCode;
// Override name to be non-enumerable
Object.defineProperty(this, 'name', {
value: 'RpcError',
writable: true,
enumerable: false,
configurable: true,
});
Object.setPrototypeOf(this, RpcError.prototype);
}
}