Plugin API
This page details the plug-in system API of VextJS, including plug-in definitions, middleware definition helper functions and related types.
Overview
Plugins are the only extension entry to the VextJS framework. Through plug-ins you can:
- Mount custom properties to
app(app.extend()) - Register global middleware (
app.use()) - Register graceful close hook (
app.onClose()) - Register readiness hook (
app.onReady()) - Register runtime lifecycle hook (
app.hooks.on()) - Replace built-in implementation (
app.setValidator()/app.setThrow()/app.setRateLimiter())
Plugin files live in src/plugins/, where plugin-loader scans them at startup. This page follows definition → lifecycle → loading → middleware helpers → types/resources. Its local examples do not form one complete project.
definePlugin
definePlugin is the recommended way to create plugins and provides type inference and IDE auto-completion support.
Function signature
Receives a VextPlugin object and returns it unchanged (for type annotation only).
Basic usage
This shows definition and close registration only. The Map has no TTL, capacity limit, or persistence. See the Plugins Guide for external resources such as Redis.
defineAppExtensions
Declare explicit generated types for values exposed through app.extend():
Export a top-level value named appExtensions from the plugin file:
At runtime this returns an empty object. It neither creates the Map, calls app.extend(), nor validates the actual value. npm exec -- vext typegen reads the declaration and generates app types; it must agree with the value mounted in setup(). See Project Structure for generated files and TypeScript integration.
VextPlugin
Plug-in interface definition.
VextPluginContext supplies config, logger, hooks, services, adapter, cache, fetch, and extension/replacement/lifecycle methods, according to lifecycle stage. Its type does not provide app.get/post/... route registration; use defineRoutes(). Custom values use a string index and may need explicit type declarations or narrowing.
VextPluginSetupContext exposes readonly signal: AbortSignal only as the second setup() argument. onReady and onClose do not receive it.
name
Plug-in name, globally unique identifier.
Used for logs, errors, and dependencies. Duplicate user plugin names fail before setup; later scanning does not overwrite earlier plugins. Use the relevant app.set*() API to replace framework capabilities, and avoid built-in extension names for custom resources.
dependencies
List of other plugin names that it depends on (optional).
plugin-loader topologically sorts user plugins so declared dependencies finish setup before the current plugin. Each dependency name must exist among user plugins scanned in this run; missing or cyclic dependencies fail startup.
This fragment assumes two user plugins named redis and sql-database mount app.redis and app.sql, and the app provides UserCacheService.
Circular dependencies can cause startup failure:
setup(app, context)
The plug-in initialization function is called by plugin-loader in step ② of bootstrap.
Parameters:
Key Notes:
- Can be a synchronous or asynchronous function
plugin-loadersets a hard timeout (default 30 seconds) for eachsetup(). Failure or timeout abortscontext.signal, rolls back controlled setup-stage framework mutations, revokes the facade, then throws. Event-loop scheduling is required; synchronous blocking code cannot be forcibly interrupted.- The setup facade is revoked after success too. A late asynchronous continuation cannot call controlled
extend,use, or lifecycle registration or assign app top-level properties. This does not stop nested object mutation or external I/O; plugins must honor cancellation and clean their own resources. - Execution order is determined by
dependenciestopological sorting app.serviceshas not been injected whensetup()is executed (service-loaderis executed afterplugin-loader), and the service cannot be accessed- If the plugin object declares
onReady(app)/onClose(app),plugin-loaderwill automatically register these two life cycle hooks aftersetup()is successful. app.hooks.on()can be used to register runtime hooks such as request/validation/response/fetch/service/plugin/OpenAPI. For details, see Application instance hooks
onReady(app) / onClose(app)
The plug-in life cycle hook is an optional field, which is equivalent to manually calling app.onReady() / app.onClose() in setup(), but the semantics are clearer.
onReady(app): In normal CLI start, runs after HTTP begins listening, useful for warming caches or reporting readiness but unable to prevent requests before listen. See Testing Guide for test-helper trigger conditions.onClose(app): executed during graceful shutdown; multiple shutdown hooks are executed in LIFO order.
Register a particular cleanup either on the object or through app.onClose(), avoiding duplicates. Setup failure/timeout rolls back setup-stage registration, so onClose alone cannot clean resources created before failure.
Plug-in loading mechanism
Automatic scanning
plugin-loader recursively scans .ts, .js, .mjs, and .cjs under src/plugins/. It skips files/directories beginning _ or ., .test./.spec. files, and .d.ts. Each loaded file should default-export a VextPlugin. Built output loads from the actual plugins/ build directory (normally dist/plugins/); JavaScript source mode loads from source.
Topological sorting
Automatically calculate the execution order based on the dependencies field:
One order is database → redis → auth. Dependency-free candidates are name-sorted; declare dependencies for required order instead of relying on filenames or scanning accidents.
Timeout protection
Each automatically loaded user plugin setup defaults to 30 seconds. Dev/start/testing entries support config.plugin.setupTimeout in milliseconds, an integer from 1 through 2,147,483,647. Timeout still aborts the signal, revokes the facade, and rolls back uncommitted mutations; pass the signal to cancellable downstream work. The manual setupPlugins callback is outside this loader deadline.
Built-in plug-ins
VextJS has a built-in monsqlize plug-in (database abstraction layer), which is created through createMonSQLizePlugin():
Although public, this factory's built-in plugin is not in the scanned user-plugin dependency graph. Enabling the built-in database does not make dependencies: ["monsqlize"] a valid user-plugin dependency. Give custom SQL pools separate config and extension names, such as sqlDatabase / sql.
defineMiddleware
Creates a middleware without configuration. It returns the original function marked with a __tag Symbol for loader recognition; the marker does not validate business logic or input data.
Function signature
Basic usage
This authentication fragment requires an app-owned verifyJWT implementation to verify signature and expiry and a req.user type extension. If also using framework RouteOptions.auth, populate req.auth; assigning a private req.user does not establish framework identity.
VextMiddleware type
Three parameters:
Onion model
The middleware implements the onion model through await next(), which can be processed before and after the handler is executed:
Call-stack order:
This is call-stack order, not a response buffering guarantee. A handler may have sent or started sending; set headers before await next() if required. A throw can skip normal after code, so use finally for work that must run on success and failure.
Short circuit response
Not calling next() can short-circuit the request and the handler will not be executed:
Error handling
Errors thrown or awaited within the request chain reach framework error-handler. Background Promises and timer errors outside that chain do not have this guarantee:
If the middleware encounters an "HTTP error that I want to actively return to the caller", it is recommended to use req.app.throw(...). If it is an unexpected runtime failure, you can also directly throw new Error("..."), and the framework will convert it to 500; if you need to return field-level verification details, you should throw VextValidationError.
defineMiddlewareFactory
Create a middleware factory with configuration. Receive configuration parameters and return the middleware function.
Function signature
Basic usage
Configuration transfer
The configuration of the middleware factory is passed through the config.middlewares whitelist:
Route references can override defaults. Route options replace the allowlist default as a whole rather than merging fields. When neither place supplies options, the factory receives undefined and must default or throw explicitly:
More examples
Client cache header middleware:
Route-level response caching does not require custom middleware. Please use the cache field of route options directly; its TTL configuration unit is milliseconds. app.cache is the response cache control surface and is only used by invalidate(), delete(), clear() and stats().
Rate limiting: use the built-in limiter:
Do not copy a permanent process-level Map into a middleware factory. Enable
Vext's built-in limiter globally and use the route override for narrower limits:
The default built-in store is process-local, so workers and application instances count independently. Configure a shared backend with rateLimit.store: { type: "redis", url } to retain the built-in algorithm and route quota overrides. Use app.setRateLimiter() when you need a custom limiter implementation; this call does not enable rate limiting by itself.
The custom branch calls only check(key) and does not automatically pass the route's max/window. The custom implementation owns its quota algorithm and window. Built-in middleware still selects keys, skips routes with false, sets response headers, and emits 429 responses. Configuration values in rate limit headers do not prove the custom algorithm applies those quotas. See Custom limiter for the complete boundary.
isMiddleware / isMiddlewareFactory
Type checking helper function, used to determine whether a value is a middleware created by defineMiddleware / defineMiddlewareFactory.
Function signature
Usage
These two functions are usually used by the middleware-loader inside the framework, and user code rarely needs to call them directly.
VextErrorMiddleware
Error middleware type (used internally by the framework).
Different from ordinary middleware, error middleware receives one more error parameter. The framework's built-in error-handler uses this type. Users usually do not need to create error middleware directly, error-handler already provides complete error handling logic.
TaggedMiddleware / TaggedMiddlewareFactory
The middleware type marked by Symbol is used by middleware-loader to distinguish between ordinary functions and framework middleware.
Symbol constant
These Symbols are automatically attached by defineMiddleware / defineMiddlewareFactory and do not need to be set manually by user code.
Middleware file organization
Directory structure
Middleware registration process
middleware-loaderscans thesrc/middlewares/directory- Match based on file name and
config.middlewareswhitelist - Use
isMiddleware()/isMiddlewareFactory()to distinguish types - Factory middleware calls
factory(options)to obtain the middleware instance - Register to the middleware definition mapping (
Map<string, VextMiddleware>) - Reference by name when registering the route
Configure whitelist
Only middleware declared in config.middlewares can be referenced in routes options.middlewares:
Middleware not declared in the whitelist will throw a startup error when referenced in a route.
Built-in middleware
VextJS provides the following built-in middleware, which is automatically registered by bootstrap and does not require manual configuration:
These middlewares can configure their behavior through config (see Configuration Items), but cannot be registered repeatedly through app.use().
Execution order
Execution order of built-in middleware (from outside to inside):
Best practices for plug-in development
1. Naming convention
- plugin
nameuses kebab-case:'my-plugin' - The file name is consistent with the plug-in name:
src/plugins/my-plugin.ts
2. Type declaration
Use declare module to provide type hints for extended properties:
3. Resource cleanup
Always clean up resources created by plugins in onClose:
4. Error handling
Errors in setup() can cause startup to fail. Ensure that critical resources are initialized with error handling:
5. Optional dependencies
If a plugin depends on an extended property of another plugin, but the dependency is optional: