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