Runtime Hooks

app.hooks.on(name, handler) is used to observe or lightweight patch framework runtime life cycle. It is suitable for cross-cutting logic such as request auditing, request records after verification, response header patch, outbound call monitoring, service call tracking, OpenAPI document patching, etc.

const off = app.hooks.on("validation:success", ({ req, route }) => {
  app.logger.info(
    { requestId: req.requestId, route: route.path },
    "validated request",
  );
});

app.hooks.on("response:before", ({ headers }) => ({
  headers: { ...headers, "x-powered-by": "vext" },
}));

off();

app.hooks.on() returns the logout function. app.hooks is a reserved property and cannot be overridden with app.extend("hooks", ...).

Common scenarios

Only record requests that pass the verification

If you want to log requests in a middleware, but exclude requests that are rejected by parameter validation, there is no need to manually catch VextValidationError. Using validation:success is more straightforward:

app.hooks.on("validation:success", ({ req, route }) => {
  app.logger.info(
    { requestId: req.requestId, method: req.method, route: route.path },
    "request validated",
  );
});

Add header before sending response

app.hooks.on("response:before", ({ headers }) => ({
  headers: {
    ...headers,
    "x-service": "billing",
  },
}));

response:before is a synchronous life cycle and cannot return Promise.

Track service calls

app.hooks.on("service:beforeCall", ({ service, method }) => {
  app.logger.debug({ service, method }, "service call");
});

app.hooks.on("service:error", ({ service, method, error }) => {
  app.logger.warn({ service, method, error }, "service failed");
});

Service hook is also a synchronous life cycle. If you need asynchronous reporting, it is recommended to write to the queue or use log transmission that does not block the main call.

Monitor outbound requests and proxies

app.hooks.on("fetch:before", ({ headers }) => {
  headers.set("x-client", "vext");
});

app.hooks.on("proxy:after", ({ target, status, requestId }) => {
  app.logger.info({ target, status, requestId }, "proxy response");
});

Modify OpenAPI documentation

app.hooks.on("openapi:afterGenerate", ({ document }) => {
  const spec = document as { info?: Record<string, unknown> };

  return {
    document: {
      ...spec,
      info: {
        ...(spec.info ?? {}),
        title: "Internal API",
      },
    },
  };
});

Execution strategy

Hook TypeStrategy
request:start, validation:success, handler:before, fetch:before, proxy:before, plugin:beforeSetup, server:beforeListenErrors thrown by the handler will propagate upward and can prevent subsequent processes
response:before, error:beforeResponse, service:beforeCall, service:afterCall, service:error, openapi:*Synchronous life cycle, return of Promise is not allowed
handler:after, handler:error, response:after, error:afterResponse, fetch:after/error, proxy:after/error, cache:*, plugin:afterSetup/error, routes:ready, app:ready/closesafe emit, hook errors will be recorded but will not change the main process

Available Hooks

NameTrigger Point
request:startAfter requestId is generated, it enters the global middleware chain; 404 will also be triggered, matched=false
route:matchedAfter the adapter matches the route and before executing the checksum handler
route:notFoundNo route matching, 404 response before sending
validation:successRoute validate all passed, before next()
validation:errorRoute validate fails and throws VextValidationError before
handler:beforeBefore the business handler is called
handler:afterAfter the business handler returns successfully
handler:errorAfter the business handler throws an error and before entering global error handling
response:beforeRuns before json/rawJson/text/html/render/stream/download/redirect; synchronously patches data/status/headers
response:afterAfter the response is sent
error:beforeResponseerror-handler can synchronize patch body/status before writing JSON error response
error:afterResponseAfter the error response is sent
fetch:beforeapp.fetch can be modified before leaving the website Headers
fetch:afterapp.fetch returns Response after
fetch:errorapp.fetch finally fails
proxy:beforeapp.fetch.proxy After parsing the upstream request and before sending it
proxy:afterapp.fetch.proxy after receiving the upstream response and before transparent transmission
proxy:errorapp.fetch.proxy on local error, timeout or upstream network failure
service:loadedAfter service is loaded and mounted during cold start
service:reloadeddev soft reload after re-instantiating service
service:beforeCallBefore the service method is called
service:afterCallAfter the service method returns successfully
service:errorAfter the service method throws an error or rejects
cache:hit, cache:miss, cache:write, cache:errorRoute-level response cache read and write life cycle
plugin:beforeSetup, plugin:afterSetup, plugin:errorPlugin setup() before and after and failure; plugins cannot observe their own beforeSetup
routes:readyAfter route scanning and registration are completed
openapi:beforeGenerate, openapi:afterGenerateBefore and after OpenAPI document generation; afterGenerate can replace document synchronously
server:beforeListenBefore HTTP server starts listening
app:readyonReady before and after execution
app:closeonClose / shutdown before and after execution

More references