Application Instance
This page details the complete API of the VextJS application instance VextApp, including built-in modules, extension methods, life cycle hooks and startup functions.
Find an API by task
The route factory's app facade exposes real application capabilities through controlled entry points and limits the lifecycle for HTTP registration. It does not copy services/config into a snapshot; request handlers retain access to those capabilities. Ownership describes which application or build process creates and releases resources/files, rather than business access permissions.
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().
Normal projects start with npm run dev, npm run build, and npm start from Quick Start; the CLI orchestrates initialization. Call bootstrap() or lower-level createApp() directly only for a custom startup flow. This page is a reference; see the complete example below for combined usage. Access app through:
- Route handler: Closure parameter of
defineRoutes((app) => { ... }) - Middleware:
req.app - Plugin setup:
setup(app)receivesVextPluginContext - Service: its
constructor(app: VextApp)receives the app
Life cycle
These are the main stages of standard HTTP bootstrap(). CLI development and testing helpers orchestrate their own lifecycles. A bare createApp() has not completed these stages:
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):
See Configuration Guide for config conditions and HTTP and Routing Specification for the request chain. Registration order and execution order are different.
Typical entry file
This is a fragment for a custom startup. A CLI project needs no extra file; running TypeScript source directly requires a suitable loader, and production should use a built project.
Return value
serverHandle exposes read-only host/port and async close(). A bind address such as 0.0.0.0 or :: is not a public URL. Use internals.shutdown(serverHandle, { skipExit: true }) to stop the full app; close() alone stops only the server.
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.
It requires a complete VextConfig; it does not load/merge config, plugins, or services, or start HTTP. The adapter is unresolved and fetch is not yet mounted as a usable client. The caller owns further initialization and cleanup.
VextApp interface
Built-in modules
app.logger
Structured log instance, implemented based on Vext’s built-in logger kernel.
Inside an enabled request context, logs carry requestId from AsyncLocalStorage. Startup logs and others outside the scope lack that request field. Runtime supplies trace(), getLevel() / setLevel(), and .child().
Log level method:
Every level accepts a message or object form, illustrated with info. Error and fatal also accept an Error object:
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.
For an unexpected runtime exception, throw new Error("...") is also caught but becomes an unknown 500 Internal Server Error. For field-level validation details, throw VextValidationError.
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 can hold caller-visible upstream error codes, messages, trace IDs, or other business fields. JSON-safe cleaning turns cycles or repeated object references into "[Circular]", Date into ISO strings, and Error into name/message. Object properties containing functions or undefined are omitted; array positions containing them become null. Prefer HttpError or app.throw for explicit details. Normalization also reads an explicitly attached details field on an ordinary exception but does not expose the entire exception object. hideInternalErrors does not filter arbitrary custom details; see Error Handling: Details.
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:
app.throw() returns never and throws at runtime. TypeScript narrowing around nested properties can be limited; return this.app.throw(404, "User not found") makes the subsequent branch explicitly handle only an existing user.
app.config
Final merged runtime configuration (read-only).
Standard startup loads default → environment config → local → bootstrap provider patch → CLI override and deeply freezes the final configuration. Production does not load local. A direct createApp(config) call does not apply that configuration-loading chain to an arbitrary object.
The standard startup freezes app.config at runtime. An attempt to modify it throws in strict mode or fails silently. For application-owned dynamic state, mount a separate object with app.extend() during plugin setup.
app.services
All service instances injected by service-loader.
Access instances through app.services.<name>. In standard startup, services load before routes, so handlers can use registered services. Plugin setup does not yet have every service, and service constructors cannot assume that other services have already been instantiated. Make cross-service calls in methods or onReady.
The CLI generates VextServices types for resolvable services. Declare them manually only for custom loading or other cases the generator cannot resolve, and include the declaration in tsconfig:
app.hooks
Framework lifecycle hook manager for registering runtime observations, lightweight patches, and cross-module integration logic.
app.hooks.on() returns an unsubscribe function. app.hooks is reserved and cannot be overridden with app.extend("hooks", ...).
Execution strategy:
Slashes in this table abbreviate multiple events; register each full event name. The built-in MonSQLize plugin:beforeSetup notification runs in its own safe synchronous initialization path. A safe listener may still be awaited when the event supports async handlers, and does not imply that the business operation succeeds. Synchronous events must not return a Promise. See Hooks: Execution strategy for multi-listener, patch, and error behavior.
Available hooks:
app:ready and app:close distinguish the two stages with phase: "before" | "after". A listener sees only events after it is registered; it cannot replay completed built-in plugin initialization. A listener removed in onClose will not see the closing after phase.
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.
VextCacheStats includes entries, hits, misses, hitRate, and underlying 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() only clears the current Vext response-cache namespace, not the entire Redis database. On shutdown, Vext closes the response-cache runtime after the user's onClose hook. See the Response Caching Guide.
app.db
The single database entry point. When config.database is present, Vext mounts
the exact raw MonSQLize instance here; without database configuration the
property remains unavailable.
app.db is not a facade or Proxy, so the complete upstream instance API is
available: collection(), model(), use(), pool(), scopedModel(),
withTransaction(), sync(), events, diagnostics, and management methods.
Vext v2 does not expose a second app.monsqlize property.
Model registry keys are exact. use() and pool() select a database or pool
scope but never prepend scope names or fall back to a transformed key. A short
name is valid only when the Model explicitly registered that key alias. Vext
owns connection cleanup during graceful shutdown; application code should not
close app.db in a second onClose hook. Use a separate extension name for application-owned SQL resources instead of overwriting this property. See the Database Guide.
app.fetch
The built-in HTTP client has type VextFetch. Standard startup mounts it before user plugin setup, with outbound requests, requestId propagation, structured logging, and proxy support. A bare createApp() return value has not mounted it yet.
Replace the example URL with your upstream. See the Fetch API for options, timeout, retries, shortcuts, and proxy, and the Fetch guide for integration. The fetch function supplied to a defineRoutes factory is bound: app.fetch(url, init) works there, but attached methods such as get, create, and proxy are not retained. Use req.app.fetch inside a handler for those methods. Plugin setup and services receive the actual app.
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
Configure these methods in plugin setup. app.use() has a defined setup window and lock check; do not assume that every set* method has the same runtime check. Plugin context is tied to setup lifecycle. After setup, work through registered callbacks instead of mutating a retained context asynchronously.
app.extend(key, value)
Mount a custom property on the app, usually during plugin setup.
defineAppExtensions provides an explicit static declaration so CLI type generation can type app.featureFlags. For custom loading that the generator cannot resolve, declare the property manually instead. Do not maintain conflicting declarations for the same property:
The key must be a nonempty valid JavaScript identifier. It cannot be reserved by the framework, shadow an inherited property, or overwrite an existing property. A repeated extend does not replace the previous value. Declared keys also check the value type; declaring a type alone does not create a runtime property.
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).
The default is schema-dsl. For this Zod example, install zod in the application first (npm install zod), then add the plugin. Vext's compile and validation functions are synchronous and cannot support refinements or transforms requiring safeParseAsync(). See the official Zod basics.
When calling the public compile(Record<string, unknown>) interface directly, use a field object. An all-Zod object goes to Zod, a pure DSL object goes to the original validator, and mixed fields fail during compilation instead of silently skipping validation. The adapter also retains a runtime branch for receiving a whole Zod schema. Replacing the engine affects subsequent compilation only; cached validators are not automatically recompiled.
setValidator() does not extend the public RouteOptions.validate type. Placing Zod fields directly into route validation currently causes a type error. This supported example validates non-HTTP service input; HTTP routes can keep DSL fields, which this plugin delegates to the original engine. Runtime compatibility does not imply route type inference support.
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 one preserving every overload and the never behavior. The example logs calls and forwards every argument. Wrapping only four positional arguments would break the i18n shortcut and object form. The error handler still determines the response body.
app.setLogger(wrapper)
Wraps or replaces the implementation of app.logger (Plugin-specific).
Receives the full runtime logger and returns a full or partial replacement. Missing methods fall back to the original logger. If you do not customize child, the framework reapplies the wrapper to the original child logger, retaining bindings and wrapping behavior. The wrapper factory may run multiple times; do not create connections repeatedly inside it.
This counts calls to the wrapped info, not log entries actually written (level filtering still applies). To forward to an external log system, use a real client and handle flushing and shutdown. Explicitly returning child: bindings => original.child(bindings) does not automatically reapply your forwarding methods to child loggers.
app.setRateLimiter(limiter)
Replaces global rate limiting implementation (plugin-specific).
This replaces the implementation but does not enable rate limiting; configure rateLimit.enabled: true too. The default flex-rate-limit implementation already supports a Redis store. If you only need shared rate-limit storage, use the rate-limit configuration. The following fragment integrates an application-owned implementation exported from src/shared/rate-limiter.ts; that module must satisfy VextRateLimiter, and the application owns connection shutdown.
VextRateLimiter interface:
resetAt is neither milliseconds nor seconds remaining. Middleware calculates the remaining seconds for RateLimit-Reset and Retry-After from it. A custom check receives only the key, not the route max/window, so the application must define its own quota policy. The framework middleware still decides whether the route disables limiting, generates the key, and sends response headers; RateLimit-Limit comes from the effective configuration.
app.setRequestIdGenerator(generate)
Override requestId generation algorithm (Plugin-specific).
The default is crypto.randomUUID(). The generator runs only when there is no nonempty inbound requestId. Precedence is the plugin generator, config.requestId.generate, then the default UUID. It is not called when requestId is disabled.
It can also be set statically through the configuration file:
Generated values and forwarded inbound headers must be strings of 1–512 characters without control characters, or validation throws. To use Nano ID or Snowflake, install and wire in the corresponding implementation. This API does not create an APM trace automatically.
Life cycle hook
app.onReady(handler)
Register a readiness hook. In standard HTTP startup it runs after listening begins. Register it before readiness processing starts; custom test orchestration controls when it runs.
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 a hook are caught and logged without stopping the service.
- Registering after readiness starts throws; a never-settling Promise blocks later readiness steps.
- Listening has already started, so initialization required before serving traffic belongs in an earlier stage such as setup.
app.onClose(handler)
Register a shutdown hook. Standard shutdown executes hooks in LIFO order. SIGTERM/SIGINT, manual shutdown, and cleanup after failed initialization can all start it. If user plugin setup fails or times out, hooks registered by that setup attempt are rolled back; the plugin must release external resources created during that attempt itself. Previously initialized resources retain their own cleanup paths. See Plugin lifecycle.
Applicable to: closing application-owned connections, refreshing log buffers, canceling scheduled tasks, etc. Vext's built-in database plugin closes app.db automatically.
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
- Registration after shutdown begins fails; all closing steps share one
shutdown.timeoutdeadline, so a callback cannot block forever.
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 internal methods returned by createApp() are used by framework startup, development mode, and test orchestration. Ordinary application code uses public lifecycle methods. Custom orchestration takes responsibility for initialization and cleanup.
shutdown process
- Idempotence: An in-progress shutdown shares one Promise; calls after shutdown has completed return immediately.
- One deadline: Start a single absolute deadline of
config.shutdown.timeoutseconds and emit theapp:closebefore notification. - Server: If a server handle exists, stop accepting requests and wait for in-flight requests.
- Cleanup: Run
onClosein LIFO order, close the response cache, emit theapp:closeafter notification, then close the logger. - Timeout and exit: After the deadline, still invoke cleanup that has not started but do not wait indefinitely. Normal completion exits with code 0;
_testModeandskipExitskip exit. A server-close failure is thrown to the caller after other cleanup; the signal handler treats it as exit code 1.
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.
This is a fragment for custom startup orchestration: internals, serverHandle, and app must come from an existing startup flow. Do not register it again after standard bootstrap. The returned cleanupSignals() removes this registration; it does not shut down the server or resources. Test mode does not register signals. When an IPC channel exists, it also listens for shutdown messages to support Windows child processes.
Auxiliary factory function
definePlugin
Recommended way to create a VextPlugin. Use defineAppExtensions for static declarations of extension properties; see the Plugin API.
defineRoutes
Core function to create routing files. See route-definition.
defineMiddleware / defineMiddlewareFactory
Helpers for middleware. The following fragments represent two separate files, each with one default export. See the Plugin API.
Type import
Complete usage example
This example uses a plugin for in-memory storage, a service for user operations, and routes consuming validated input. It needs no database or third-party plugin. Data is lost when the process exits and the endpoints are public; add persistence and authorization for production as described in the respective guides.
Start with the TypeScript project in Quick start, retaining its dev/build/start scripts and .vext/types in tsconfig. The following four files form a standalone example; do not stack them on top of another user service or users route.
Plugin development
Service Development
Routing development
Run and observe
The CLI generates extension and service types. Once ready and the listening address appear, make requests from another terminal. The plugin logs its initial count of 1 during onReady; a CLI startup summary may collapse that log, so confirm availability with the responses:
The first two return 200, the missing user 404, and invalid page 422. Creation returns 201; repeating the same email returns 409 with business code 10001. Omitting name or email returns 422. Successful data is in data, and the list defaults to page 1 with limit 20. On Windows PowerShell, use curl.exe for GET; for creation you can run:
After Ctrl+C, the store-cleared log should report count 0. Run npm run build and npm start, then repeat the requests to check the built entry point. Stop any existing example process on port 3000 or change the port and request URLs. Each process has its own in-memory data.
If extension types are missing, check CLI type generation and the .vext/types/**/*.d.ts entry in tsconfig. If a business route returns 404, check its file directory and /users prefix. Continue with Services, Plugins, Database, and Security for a real application.