Client

Modern client for mion APIs:

  • Strongly typed APIs with autocompletion and static type checking.
  • Fully typed list of remote methods with it's parameters and return values.
  • Automattic 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.

End to End Type Safety

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.

At the moment the client makes an extra request on initialization to get routes and hooks metadata required for local validation and serialization.
import {initClient} from '@mionkit/client';

// importing only the RemoteApi type from server
import type {MyApi} from './server.routes';
import {ParamsValidationResponse} from '@mionkit/reflection';

const {routes, hooks} = initClient<MyApi>({baseURL: 'http://localhost:3000'});

Calling remote routes

The methods object returned from initClient contains all remote routes and hooks. If a hook is not public (does not expect any parameters and can't return data) then it is not included within the methods object.

Calling routes and hooks in the client generates a RouteSubRequest or HookSubRequest. This is so multiple HookSubRequest can be added to a single RouteSubRequest. We use the call() method from RouteSubRequest to perform the remote call, you can pass any required HookSubRequest data to this call method.

// calls sumTwo route in the server
const authSubRequest = hooks.auth('myToken-XYZ');
const sumTwoResp = await routes.utils.sum(5, 2).call(authSubRequest);
console.log(sumTwoResp); // 7

Prefilling Hooks data

For cases like authorization hooks that are required for all request, we can use the prefill() method of HookSubRequest. This will automatically persist the subRequest in local storage and prefill any future route calls that require that hook.

// prefills the token for any future requests, value is stored in localStorage
await hooks.auth('myToken-XYZ').prefill();
// // calls sumTwo route in the server
const sumTwoResponse = await routes.utils.sum(5, 2).call();
console.log(sumTwoResponse); // 7

Full example

Client
import {initClient} from '@mionkit/client';
// importing only the RemoteApi type from server
import type {MyApi} from './server.routes';

const john = {id: '123', name: 'John', surname: 'Doe'};
const {routes, hooks} = initClient<MyApi>({baseURL: 'http://localhost:3000'});

// prefills auth token for any future requests, value is stored in localStorage by default
await hooks.auth('myToken-XYZ').prefill();

// calls sayHello route in the server
const sayHello = await routes.users.sayHello(john).call();
console.log(sayHello); // Hello John Doe

// validate parameters locally without calling the server (await still required as validate is async)
const validationResp = await routes.users.sayHello(john).validate();
console.log(validationResp); // {hasErrors: false, totalErrors: 0, errors: []}
server.routes.ts
import {RpcError} from '@mionkit/core';
import {Routes, headersHook, hook, 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};

const routes = {
    auth: headersHook('authorization', (ctx, token: string): void => {
        if (!token) throw new RpcError({statusCode: 401, message: 'Not Authorized', name: ' Not Authorized'});
    }),
    users: {
        getById: route((ctx, id: string): User => ({id, name: 'John', surname: 'Smith'}))),
        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: {
        getById: route((ctx, id: string): Order => ({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: hook((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 = initMionRouter(routes);

// Export the type of the Api (used by the client)
export type MyApi = typeof myApi;

Type Reference

RemoteApi

This type could be considered your API schema to be used by the client. This is a Mapping of your public hooks and routes to PublicProcedure

export type RemoteApi<Type extends Routes> =  // ... Maps Public Hooks and Routes to PublicProcedure

PublicProcedure

Required Metadata from Hook and Routes required for validation and serialization in the client.

export interface PublicProcedure<H extends Handler = any> {
    type: ProcedureType;
    /** Type reference to the route handler, it's runtime value is actually null, just used statically by typescript. */
    handler: PublicHandler<H>;
    /** Json serializable structure so the Type information can be transmitted over the wire */
    serializedTypes: SerializedTypes;
    id: string;
    useValidation: boolean;
    useSerialization: boolean;
    params: string[];
    hookIds?: string[];
    pathPointers?: string[][];
    headerName?: string;
}

SubRequest

export interface SubRequest<RM extends PublicProcedure> {
    pointer: string[];
    id: RM['id'];
    isResolved: boolean;
    params: Parameters<RM['handler']>;
    return?: HandlerSuccessResponse<RM>;
    error?: HandlerFailResponse<RM>;
    validationResponse?: ParamsValidationResponse;
    serializedParams?: any[];
    // note this type can't contain functions, so it can be stored/restored from localStorage
}

RouteSubRequest

export interface RouteSubRequest<RR extends PublicRouteProcedure> extends SubRequest<RR> {
    /**
     * Validates Route's parameters. Throws RpcError if validation fails.
     * @returns {hasErrors: false, totalErrors: 0, errors: []}
     */
    validate: () => Promise<ParamsValidationResponse>;
    /**
     * Calls a remote route.
     * Validates route and required hooks request parameters locally before calling the remote route.
     * Throws RpcError if anything fails during the call (including validation or serialization) or if the remote route returns an error.
     * @param hooks HookSubRequests requires by the route
     * @returns
     */
    call: <RHList extends HookSubRequest<any>[]>(...hooks: RHList) => Promise<HandlerSuccessResponse<RR>>;
}

HookSubRequest

export interface HookSubRequest<RH extends PublicHookProcedure | PublicHeaderProcedure> extends SubRequest<RH> {
    /**
     * Validates Hooks's parameters. Throws RpcError if validation fails.
     * @returns {hasErrors: false, totalErrors: 0, errors: []}
     */
    validate: () => Promise<ParamsValidationResponse>;
    /**
     * Prefills Hook's parameters for any future request. Parameters are also persisted in local storage for future requests.
     * Validates and Serializes parameters before storing in local storage.
     * Throws RpcError if validation or serialization fail or if the parameters can't be persisted.
     * @returns Promise<void>
     */
    prefill: () => Promise<void>;

    /**
     * Removes prefilled value.
     * Throws RpcError if something fails removing the prefilled parameters
     * @returns Promise<void>
     */
    removePrefill: () => Promise<void>;
}