Client

Client Overview

Learn how to use the mion client for type-safe API calls.

Features

  • ✅ Strongly typed APIs with autocompletion and static type checking.
  • ✅ Fully typed list of remote methods with its parameters and return values.
  • ✅ Result pattern with 4-tuple [routeResult, routeError, middleFnsResults, middleFnsErrors] - never throws, always returns.
  • ✅ Automatic Validation and Serialization out of the box.
  • ✅ Local Validation (no need to make a server request to validate parameters)
  • ✅ Prefill request data to persist across multiple calls.

Using the client

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'});
At the moment the client makes an initialization request to get routes and middleFns metadata required for local validation and serialization. Use AOT compilation to avoid this initialization request.

Calling Routes

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
    }
}

Using MiddleFns

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
}

Prefilling MiddleFns data

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:

  1. TypedEvent handlers - Strongly typed by errors
  2. tuple results - Generic RpcError types
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
}
TypedEvent handlers are called after every API request that includes the middleFn.
Use TypedEvent handlers for global error handling with prefilled middleFns. They provide strongly typed error handling based on the error type string.

Full example

/* 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); // []
}

Type Reference

Result

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

CallWithMiddleFnsResult

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

RemoteApi

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;
};

SubRequest

// Error processing code-import:
// Start comment marker not found: // type-sub-request-start

RouteSubRequest

// Error processing code-import:
// Start comment marker not found: // type-route-sub-request-start

MiddleFnSubRequest

// Error processing code-import:
// Start comment marker not found: // type-middleFn-sub-request-start