Application example
This page details the complete API of the VextJS application instance VextApp, including built-in modules, extension methods, life cycle hooks and startup functions.
Overview
VextApp is the core object of the entire VextJS application, created through createApp(config). It mounts built-in capabilities such as configuration, services, logging, and error throwing, and supports plug-in extensions through methods such as extend() / use().
In most scenarios, you don't need to call createApp() directly - bootstrap() will call it automatically internally. You access app via:
- Route handler: Closure parameter of
defineRoutes((app) => { ... }) - Middleware:
req.app - Plug-in setup: Parameters of
setup(app) - Services: access each other through
app.services
Life cycle
VextApp goes through the following stages from creation to destruction:
bootstrap
bootstrap() is the standard startup function of the framework, arranging a complete startup process.
Function signature
Parameters
Start the process
bootstrap() internally performs the following steps (in order):
Typical entry file
Return value
createApp
createApp() is the underlying factory function that creates VextApp instances and a collection of framework internal methods.
Function signature
Return value
Normally there is no need to call createApp() directly. bootstrap() and createTestApp() have encapsulated the complete initialization process internally. Only use this function if you need to completely customize the startup process.
VextApp interface
Built-in modules
app.logger
Structured log instance, implemented based on Vext’s built-in logger kernel.
Automatically carry requestId (through AsyncLocalStorage), support trace(), runtime getLevel() / setLevel() and .child() to create child logger.
Log level method:
Each method supports two signatures:
getLevel() / setLevel(level):
setLevel() only affects subsequent logs; created child loggers share the current runtime level with the parent logger. The default logger does not provide a writable app.logger.level property.
child(bindings):
Create a child logger with additional context fields. All logs output through child loggers will automatically have the fields in bindings appended.
app.throw(status, message, paramsOrCode?, codeOrDetails?)
When an HTTP error is thrown, the framework uniformly converts it to a standard error response. Three calling forms are supported.
app.throw()
app.throw() is suitable for scenarios where "I want to actively return a clear HTTP error to the caller", such as 401, 404, 409 or a response with a business error code.
If an unexpected runtime exception occurs, you can also directly throw new Error("..."), and the framework will also catch it, but this type of error will enter an unknown exception path and eventually turn into 500 Internal Server Error. If field-level validation details need to be returned, VextValidationError` should be thrown.
Function signature:
Shortcut (recommended for i18n scenarios)
When the first parameter is a string, it is regarded as an i18n key shortcut call. The HTTP status code is read from the statusCode field configured in the i18n language package. If not configured, the default is 400:
Status parsing rules for shortcuts:
Business error code of shortcut: If the i18n language package is configured with an independent code for the key (different from the key itself), it will be automatically appended to the response.
Standard call
When the first parameter is a number, as an HTTP status code, the behavior is exactly the same as before:
Standard calling parameters:
details is suitable for storing business details returned by third-party interfaces, such as upstream error codes, original messages, trace ids or fields that can be displayed to the caller. The framework will do JSON-safe cleaning before responding: circular references will become "[Circular]", Date will output ISO strings, Error will only output name/message, and functions and undefined will not appear in the response. Unknown plain Error details are not automatically exposed and must be passed in explicitly via HttpError or app.throw.
i18n linkage
message (or messageKey for shortcuts) also serves as the i18n key for language pack lookup. The framework obtains the locale of the current request through AsyncLocalStorage and automatically translates the error message:
When there is no i18n language pack, it degrades to the original message and is passed directly.
Error response format:
The return type of app.throw() is never, which means it interrupts the current function execution. No need to add a return statement after the call. The TypeScript type system correctly identifies subsequent code as unreachable.
app.config
Final merged runtime configuration (read-only).
The default → env → local → bootstrap provider patch → CLI override configuration chain is loaded by loadConfig() and deep frozen.
app.config is frozen at runtime and any attempt to modify it will throw an error (strict mode) or fail silently. For dynamic configuration, use app.extend() to mount mutable state.
app.services
All service instances injected by service-loader.
Access via app.services.<name>. service-loader is executed before router-loader, so it is safe to access app.services in the handler.
Type extension:
app.hooks
Framework lifecycle hook manager for registering runtime observations, lightweight patches, and cross-module integration logic.
app.hooks.on() returns the logout function. app.hooks is a reserved property and cannot be overridden by app.extend("hooks", ...).
Execution Strategy:
| Available hooks: | Name | Trigger Point |
| --------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | ------------- | -------------- | --------------------------------------------------- |
| request:start | After requestId is generated, it enters the global middleware chain; 404 will also be triggered, matched=false |
| route:matched | After the adapter matches the route and before executing the checksum handler |
| route:notFound | No route matching, 404 response before sending |
| validation:success | Route validate all passed, before next() |
| validation:error | Route validate fails and throws VextValidationError before |
| handler:before | Before the business handler is called |
| handler:after | After the business handler returns successfully |
| handler:error | After the business handler throws an error and before entering global error handling |
| response:before | Runs before json/rawJson/text/html/render/stream/download/redirect; synchronously patches data/status/headers |
| response:after | After the response is sent |
| error:beforeResponse | error-handler can synchronize patch body/status before writing JSON error response |
| error:afterResponse | After the error response is sent |
| fetch:before | app.fetch can be modified before leaving the website Headers |
| fetch:after | app.fetch returns Response after |
| fetch:error | app.fetch finally fails |
| proxy:before | app.fetch.proxy After parsing the upstream request and before sending it |
| proxy:after | app.fetch.proxy after receiving the upstream response and before transparent transmission |
| proxy:error | app.fetch.proxy on local error, timeout or upstream network failure |
| service:loaded | After service is loaded and mounted during cold start |
| service:reloaded | dev soft reload after re-instantiating service |
| service:beforeCall | Before the service method is called |
| service:afterCall | After the service method returns successfully |
| service:error | After the service method throws an error or rejects |
| cache:hit, cache:miss, cache:write, cache:error | Route-level response cache read and write life cycle |
| plugin:beforeSetup, plugin:afterSetup, plugin:error | Plugin setup() before and after and failure; plugins cannot observe their own beforeSetup | | routes:ready | After route scanning and registration are completed |
| openapi:beforeGenerate, openapi:afterGenerate | Before and after OpenAPI document generation; afterGenerate can replace document synchronously |
| server:beforeListen | Before HTTP server starts listening |
| app:ready | onReady before and after execution |
| app:close | onClose/shutdown before and after execution |
If you only want to record "requests that pass parameter verification", use validation:success. In this way, requests that fail verification will not enter this hook, which is more direct than manually excluding VextValidationError in ordinary global middleware.
app.cache
Route-level response cache management API. Initialized in the createApp stage, it provides operations such as label invalidation, specified key deletion, clearing, and statistics.
app.cache is Vext's control surface wrapper for response-cache-kit; business code does not need to directly operate the underlying Store. In Redis/MultiLevel mode, clear() will not clear the entire Redis library, but will only clear the current vext response cache namespace. When shutdown is applied, Vext will close the response cache runtime resource after the user's onClose hook is executed. See the Response Caching Guide for details.
app.adapter
The underlying adapter instance (mounted after being resolved by resolveAdapter()).
This is a framework internal property and user code usually does not need to manipulate the adapter directly. The framework registers middleware, routing, error handling, etc. through adapter.
HTTP method
The HTTP methods on VextApp (get/post/put/patch/delete/head/options) are placeholder methods and cannot be called directly. The actual route registration is done through defineRoutes.
Supports three-paragraph and two-paragraph two syntaxes:
Supported methods: get / post / put / patch / delete / head / options
Framework extension API
app.extend(key, value)
Mount custom properties to the app (Plug-in only).
Use with declare module to get type hints:
app.use(middleware)
Register global HTTP middleware (plugin-specific).
Effective for all routes, executed before route-level middlewares. It can only be called in plug-in setup(). The call will throw an error after the route registration is completed.
For app-wide browser security headers, prefer config.securityHeaders because it also covers errors, 404 responses, testing helpers, and dev soft reload. Manual app.use(securityHeaders()) is a scoped plugin entry.
app.use() will be locked after route registration (router-loader) is completed. Calls after this will throw an error:
app.setValidator(validator)
Replace the global validation engine (Plug-in only).
By default, schema-dsl is used, which can be replaced by third-party verification libraries such as Zod and Yup.
app.getValidator()
Get the current global verification engine instance.
The default validator is implemented based on schema-dsl. Plug-ins can replace it with Zod, Yup, etc. implementations through app.setValidator(), so getValidator() is not equivalent to a fixed schema-dsl, but always returns the currently valid validator.
It can also be reused when handling non-HTTP input in the service:
app.setThrow(wrapper)
Wraps or replaces the implementation of app.throw (Plugin-specific).
Receives the original throw implementation and returns the new implementation. Can be used to intercept errors, add logs, modify error formats, etc.
app.setLogger(wrapper)
Wraps or replaces the implementation of app.logger (Plugin-specific).
Receive the complete runtime logger and return a complete or partial new logger. Missing methods fall back to the original logger, including trace, getLevel, setLevel and child. Common uses: Forward framework logs to external systems (OTel Logs, Sentry, etc.) simultaneously.
The @devcodex/opentelemetry plug-in has this mode built-in. After turning on logs.bridgeAppLogger: true (default), app.setLogger() is automatically called without manual implementation.
app.setRateLimiter(limiter)
Replaces global rate limiting implementation (plugin-specific).
By default flex-rate-limit is used. Can be replaced with a Redis implementation to support distributed throttling.
VextRateLimiter interface:
app.setRequestIdGenerator(generate)
Override requestId generation algorithm (Plugin-specific).
By default crypto.randomUUID() is used. Common replacements: APM traceId, Snowflake ID, etc.
It can also be set statically through the configuration file:
Life cycle hook
app.onReady(handler)
Register a readiness hook to be executed after HTTP listening starts.
Suitable for: preheating cache, checking external dependencies, printing startup information, etc.
Execution Rules:
- All
onReadyhooks are executed sequentially in the order in which they were registered (not in parallel) - Automatically clear the hooks array and release the closure reference after execution is completed
- Errors thrown in the hook will be captured and logged and will not affect service operation.
app.onClose(handler)
Register graceful shutdown hooks and execute them in LIFO order when the SIGTERM/SIGINT signal is triggered.
Applicable to: closing database connections, refreshing log buffers, canceling scheduled tasks, etc.
Execution Rules:
- Executed in LIFO (last in, first out) order - hooks registered later are executed first
- Each hook has an independent try/catch, and the failure of a single hook does not affect other hooks
- Automatically clear the hooks array and release resource references after execution is completed
LIFO sequential design reasons:
Resources should be destroyed in the reverse order of creation. For example: connect to the database first, and then create a cache based on the database. When closing, you should first close the cache and then close the database.
AppInternals
The set of internal methods returned by createApp() is only used by bootstrap and should not be called directly by user code.
shutdown process
- Anti-duplication: The internal
_shuttingDownflag prevents SIGTERM + SIGINT from being triggered repeatedly - Step 1: Stop accepting new requests + wait for in-flight requests to complete (protected by
config.shutdown.timeouttimeout) - Step 2: Execute all
onClosehooks in LIFO order (each hook has an independent try/catch) - Step 3: Exit the process (skip
process.exit()when using_testModeorskipExit)
DEFAULT_CONFIG
The framework has built-in default configuration constants that can be used for reference or quick start:
See Configuration API — DEFAULT_CONFIG for complete details.
setupShutdown
Independent signal processing registration function, automatically called internally by bootstrap.
Register SIGTERM and SIGINT signal handlers and trigger internals.shutdown() when the signal is received.
Auxiliary factory function
definePlugin
Recommended way to create a VextPlugin. See plugin API.
defineRoutes
Core function to create routing files. See route-definition.
defineMiddleware / defineMiddlewareFactory
Create helper functions for middleware. See plugin API.