Hooks

Hooks are simply auxiliary functions that are executed before or after a route gets executed. We call them hooks because they are methods that get hooked into the Execution Path of a route.

Hooks are the equivalent to middleware in some other frameworks.

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, HookDef, registerRoutes} from '@mionkit/router';
import {myApp} from './myApp';

const logger = {
    // ensures logger is executed even if there are errors in the route or other hooks
    forceRunOnError: true,
    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);
    },
} satisfies HookDef;

registerRoutes({
    // ... other routes and hooks
    logger, // logs things after all other hooks and routes are executed
});

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.

As headers are always strings, when using a header hook Soft Type Conversion is used, 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 {HeaderHookDef, registerRoutes} from '@mionkit/router';
import {getAuthUser, isAuthorized} from 'MyAuth';

const auth = {
    // headerName is required when defining a HeaderHook
    headerName: 'authorization',
    hook: 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
    },
} satisfies HeaderHookDef;

registerRoutes({
    auth,
    // ... other routes and hooks. If auth fails they wont get executed
});

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 {CallContext, RawHookDef, registerRoutes} from '@mionkit/router';
import {IncomingMessage, ServerResponse} from 'http';
type HttpRequest = IncomingMessage & {body: any};

// sends a fake progress to the client
const progress = {
    // isRawHook = true, required when defining a RawHook
    isRawHook: true,
    hook: async (ctx: CallContext, 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);
        });
    },
} satisfies RawHookDef<any, HttpRequest, ServerResponse>;

registerRoutes({
    progress,
    // ... other routes and hooks
});

Force Run On Errors

When there is an error in a route or hook the rest of hooks are not executed unless forceRunOnError 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 logger = {
    // ensures logger is executed even if there are errors in the route or other hooks
    forceRunOnError: true,
    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);
    },
} satisfies HookDef;

Type Reference

HookBase (HookOptions)

interface HookBase {
    /** Executes the hook even if an error was thrown previously */
    forceRunOnError?: boolean;
    /** Description of the route, mostly for documentation purposes */
    description?: string;
    /** Overrides global enableValidation */
    enableValidation?: boolean;
    /** Overrides global enableSerialization */
    enableSerialization?: boolean;
}

HookDef

export interface HookDef<Context extends CallContext = CallContext, Ret = any> extends HookBase {
    /** Hook handler */
    hook: Handler<Context, Ret>;
}

HeaderHookDef

export interface HeaderHookDef<
    Context extends CallContext = CallContext,
    HReqValue extends HeaderValue = any,
    HRespValue extends HeaderValue = any,
> extends HookBase {
    /** the name of the header in the request/response */
    headerName: string;
    hook: HeaderHandler<Context, HReqValue, HRespValue>;
}

RawHookDef

export interface RawHookDef<
    Context extends CallContext = CallContext,
    RawReq = unknown,
    RawResp = unknown,
    Opts extends RouterOptions<RawReq> = RouterOptions<RawReq>,
> {
    isRawHook: true;
    hook: RawHookHandler<Context, RawReq, RawResp, Opts>;
}