Hooks (Middleware)

Hooks or (Middleware functions) are executed before or after a route gets executed. We call them hooks because they are functions that get hooked into the Execution Path of a route and because 'Middleware' is too long to write ๐Ÿ˜‚.

Hooks are useful when a route might require some extra data like authorization, filters or some extra processing like body parsing, logging, etc...

Same as routes the first parameter is always the Call Context, the rest of parameters are remote parameters that get deserialized and validated before the hook gets executed.

Defining a Hook

Unlike routes we can't define a Hook using a simple function Handler we must use a HookDef object. This is so typescript can statically know which router entries are hooks and which ones are routes.

Quick Tip:
never assign a type to Hooks, instead always use the satisfies operator!
import {CallContext, hook, Routes} from '@mionkit/router';
import {myApp} from './myApp';

const routes = {
    // using the hook function to define a hook
    logger: hook(
        async (ctx: CallContext): Promise<void> => {
            const hasErrors = ctx.request.internalErrors.length > 0;
            if (hasErrors) await myApp.cloudLogs.error(ctx.path, ctx.request.internalErrors);
            else myApp.cloudLogs.log(ctx.path, ctx.shared.me.name);
        },
        // ensures logger is executed even if there are errors in the route or other hooks
        {runOnError: true}
    ),
    // ... other routes and hooks
} satisfies Routes;

Header Hooks

For cases were we nee to send or receive data in http headers we can use a HeaderHookDef. These hooks are limited to just one remote parameter besides the context and must be of type string, number or boolean. No other kind of data is allowed in headers.

Soft Type Conversion is used for headers as those are always strings, this means strings like '1' , '0', 'true', 'false' will be converted to boolean and numeric strings like '5' , '100' will be converted to a number.

Please note header names are NOT case sensitive!
Any header name Authorization or AUTHORIZATION will match the Hook with header name authorization.
import {headersHook, Routes} from '@mionkit/router';
import {getAuthUser, isAuthorized} from 'MyAuth';

const routes = {
    // using the headersHook function to define a hook
    auth: headersHook('authorization', async (ctx, token: string): Promise<void> => {
        const me = await getAuthUser(token);
        if (!isAuthorized(me)) throw {code: 401, message: 'user is not authorized'};
        ctx.shared.auth = {me}; // user is added to ctx to shared with other routes/hooks
    }),
    // ... other routes and hooks
} satisfies Routes;

Raw Hooks

In case we need to access the raw or native underlying request and response, we must use a RawHookDef.

These are hooks that receive the CallContext , RawRequest , RawResponse and RouterOptions, but can't receive any remote parameters or return any data, raw Hooks can only modify the CallContext and return or throw errors.

Raw Hooks are useful to extend the router's core functionality, i.e: The router internally uses a Raw Hook to parse and stringify the JSON body.

import {rawHook, Routes} from '@mionkit/router';
import {IncomingMessage, ServerResponse} from 'http';
type HttpRequest = IncomingMessage & {body: any};

const routes = {
    // using the rawHook function to define a hook
    progress: rawHook(async (ctx, rawRequest: HttpRequest, rawResponse: ServerResponse): Promise<void> => {
        return new Promise((resolve) => {
            const maxTime = 1000;
            const increment = 10;
            let total = 0;
            const intervale = setInterval(() => {
                if (total >= maxTime) {
                    clearInterval(intervale);
                    resolve();
                }
                total += increment;
                rawResponse.write(`\n${total}%`);
            }, increment);
        });
    }),
    // ... other routes and hooks
} satisfies Routes;

Force Run On Errors

When there is an error in a route or hook the rest of hooks are not executed unless runOnError is set to true. This is useful for some hooks like a logger that needs to be executed after any other hook and log all the errors or request data.

const routes = {
    // using the hook function to define a hook
    logger: hook(
        async (ctx: CallContext): Promise<void> => {
            const hasErrors = ctx.request.internalErrors.length > 0;
            if (hasErrors) await myApp.cloudLogs.error(ctx.path, ctx.request.internalErrors);
            else myApp.cloudLogs.log(ctx.path, ctx.shared.me.name);
        },
        // ensures logger is executed even if there are errors in the route or other hooks
        {runOnError: true}
    ),
    // ... other routes and hooks
} satisfies Routes;

Type Reference

HookBase (HookOptions)

 * Raw hook, used only to access raw request/response and modify the call context.
 * Can not declare extra parameters.
 */
export type RawHookDef<H extends RawHookHandler = any> = Pick<
    RawProcedure<H>,
    'type' | 'handler' | 'runOnError' | 'canReturnData' | 'useSerialization' | 'useValidation'
>;

HookDef

/** Hook definition, a function that hooks into the execution path */
export type HookDef<H extends Handler = any> = Pick<HookProcedure<H>, 'type' | 'handler' | 'runOnError'>;

HeaderHookDef

export type HeaderHookDef<H extends HeaderHandler = any> = Pick<
    HeaderProcedure<H>,
    'type' | 'handler' | 'runOnError' | 'headerName'
>;

RawHookDef

export type RawHookDef<H extends RawHookHandler = any> = Pick<
    RawProcedure<H>,
    'type' | 'handler' | 'runOnError' | 'canReturnData' | 'useSerialization' | 'useValidation'
>;