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.
Register hooks in plugin setup. Run the three-file example first, then combine the topic fragments as needed. Each event has its own error and sync rules; callers await or synchronously execute listeners according to that rule, so slow listeners affect the original flow.
Complete three-file example
Start from the API project, TypeScript configuration, and dev/build/start scripts in Quick start. Replace the base config and create the two files below. No database or monitoring service is required. Use an isolated example project; if you retain environment overrides, ensure the final port is 3000 and the log level permits info.
Run npm run dev, then issue these requests in order from another terminal (use curl.exe in Windows PowerShell):
Expected statuses are 200 / 422 / 200 / 500. The first response has data.message equal to Hello, Alice!; all responses carry x-hook-example: active. The missing-query request never reaches a handler. plain has no validate option and does not emit validation:success; boom hides its internal message by default.
After four requests, stop the service normally with Ctrl+C. The Hook example stopped log should show validated=1, rejected=1, handled=2, errors=1, and validationListenerPresent=false. This assumes an isolated project with no other requests or listeners; repeated calls change counts, and another plugin could keep has true. Force-killing the process does not verify shutdown callbacks.
Run npm run build (the quick-start script includes vext build --typecheck), then npm start, repeat the requests, and stop normally to check the built app. appExtensions supplies the type-generation shape of hookStats; it does not create runtime state.
Register and unsubscribe
The following fragment belongs inside plugin setup:
app.hooks.on() returns an unsubscribe function. app.hooks is reserved and cannot be overridden with app.extend("hooks", ...). Here off() removes only the validation listener, so it receives no later event; the response listener remains. Register each long-lived remover with app.onClose(), and remove temporary listeners when done.
app.hooks.has(name) checks whether any listener currently exists. The public API is on and has; emission methods are framework internals. A plugin should release its own listeners on close or reload.
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:
This event runs only when a route declares effective validation and the request reaches it. It does not count every successful request: auth rejection, earlier short-circuiting, or cache hits can bypass validation. Successful validation also does not guarantee the later handler succeeds.
Add header before sending response
response:before is a synchronous life cycle and cannot return Promise.
Track service calls
Service hooks are synchronous. A service:beforeCall exception prevents the method call; afterCall and error are safe synchronous notifications that do not replace the business result. They cover framework-wrapped prototype methods, not instance arrow functions or getters. Use a bounded queue with failure and shutdown flushing policies for asynchronous reporting; do not attach an async listener to a synchronous event.
Monitor outbound requests and proxies
Modify OpenAPI documentation
Execution strategy
Slash forms in the table abbreviate distinct event names; do not pass the abbreviation to on. Built-in MonSQLize plugin:beforeSetup is a safe synchronous notification in a separate startup flow, unlike user plugin setup.
All synchronous events reject Promise returns, even where the public TypeScript generic cannot yet prevent an async listener. Detecting a Promise does not cancel async work that already started. Safe means listener exceptions are logged, not that an async listener costs no time or is never awaited, nor that business work succeeded. Avoid never-settling Promises in hooks.
Multiple listeners and patches
Listeners execute in registration order, and a Set deduplicates the same function reference. For events with a return value, the last non-undefined result wins: patches returned by multiple listeners are not merged, and an earlier return does not become the next listener's payload. Return one consolidated patch when changing data, status, and headers together. Mutating a mutable payload directly is a different behavior and needs an explicit coordination boundary.
OpenAPI accepts a synchronous { document } result or a complete document containing an openapi field. Returning only an info fragment is not a full replacement. Hooks do not replace static route contracts; see the app.hooks API.
Available Hooks
Listeners observe only events after registration. User plugins cannot replay built-in MonSQLize initialization or their own already completed plugin:beforeSetup. Count app:ready and app:close by phase; a listener removed in onClose will not see the later close-after event.
Troubleshooting and recheck
More references
app.hooksAPI- [Register runtime hooks in plug-ins](/guide/plugins#apphookson--Register runtime life cycle-hook)
- Fetch / Proxy hooks
- OpenAPI hooks