Adapter architecture
VextJS uses an Adapter architecture to replace the underlying HTTP processing layer. Routes and services written against VextJS req / res can generally retain their interfaces; framework-specific middleware, plugins, and features still require an integration check. This page first verifies installation, configuration, startup, and switching, then explains the custom Adapter interface.
Working principle
Adapter is responsible for:
- Start HTTP service — Use the underlying framework to create a server and listen on the port
- Request Conversion — Convert the native request object of the underlying framework into
VextRequest - Response conversion — Map the operations of
VextResponseto the response object of the underlying framework - Route Registration — Register the routes collected by the framework to the underlying routing system
- Middleware execution — Collect global middleware and compose the route execution chain under VextJS conventions
Built-in Adapter
VextJS has 5 built-in Adapters, covering the mainstream Node.js HTTP framework:
These ranges come from the current VextJS package declaration; use the installed version's peer requirements when upgrading. Selecting an Adapter does not automatically expose that framework's native plugin registration API.
Performance comparison
This page does not keep a separate numeric snapshot. The public benchmark uses the same lightweight Vext Normal application and changes only the five supported Adapters. It compares Vext Adapter integration paths; it does not measure each underlying framework's independent Raw performance or Vext overhead percentages against Raw baselines.
See Performance benchmarks for the retained results, methodology, limitations, and reproduction commands. The public sample is from 2026-08-15 at a specified Vext 1.0.1 commit; it is not a performance baseline for the current 2.0.0 source. After choosing an adapter, validate it with your real middleware, authentication, logging, and I/O workload.
How to use
Verify one route first
Prerequisites: prepare Node.js, ESM package.json, TypeScript configuration, and dev/build/start scripts using Quick Start's manual setup. This API-only example needs no database or external service. Merge configuration into an existing project and avoid a route filename collision.
The filename contributes /adapter-demo; use only /:id and / inside to avoid duplicating that prefix. Run npm run dev, then from another terminal (use curl.exe in PowerShell):
Expect 200 with data: { "id": "u-1", "adapter": "native" }, then 201 with data.name: "Alice", then a 422 validation error, then 404. Successful responses also have code: 0 and requestId by default. Stop dev with Ctrl+C; run npm run build and npm start, repeat all four requests against production output, then stop the service to release the port. Configuration fragments below show Adapter options only; merge them with your other fields.
Native Adapter (default)
No additional dependencies need to be installed, and no explicit configuration is required - the default is Native Adapter:
For explicit declaration:
The Native adapter uses Node.js http.createServer with route-core. It is the default path and has no third-party HTTP framework dependency. Performance varies by workload, so use the current benchmark and your application tests when making a decision.
Hono Adapter
Recommended method (string identification):
Advanced usage (factory function):
Hono is an ultra-lightweight web framework based on the Web Standards API (Request / Response). The current built-in Hono Adapter is a Node.js HTTP server adapter. It depends only on hono; Vext owns the node:http request/response bridge used to expose Hono routing inside a Node.js service. @hono/node-server is not a runtime dependency of this adapter.
This does not represent official Edge / Serverless adapter support. Cloudflare Workers, Deno Deploy, Bun edge, and other non-Node.js runtimes require a dedicated Edge / Serverless adapter or ecosystem plugin. Do not treat the current vextjs/adapters/hono package as an Edge runtime guarantee.
Fastify Adapter
Recommended method (string identification):
Advanced usage (factory function, options can be passed in):
VextJS uses Fastify to host routes and the HTTP server, while Vext's own pipeline handles validation and JSON serialization. res.json() is serialized by Vext before sending; selecting Fastify does not automatically adopt Fastify route schemas or plugins. To set supported options, use a factory such as fastifyAdapter({ caseSensitive: true }); it accepts FastifyAdapterOptions, not arbitrary Fastify configuration.
Express Adapter
Recommended method (string identification):
Advanced usage (factory function, options can be passed in):
The current implementation uses Express v5. Business logic independent of HTTP objects can be reused in a migration; native Express routes and (req, res, next) middleware need adaptation to Vext interfaces. ExpressAdapterOptions exposes a string bodyLimit, not an entire Express application instance.
An existing Express v4 dependency does not satisfy this Adapter's peer range. Check the application's dependencies and migration impact before installing a compatible version; changing only the adapter string does not complete a migration.
Koa Adapter
Recommended method (string identification):
Advanced usage (factory function, options can be passed in):
The current implementation uses Koa v3 with @koa/router for route matching; install both packages. KoaAdapterOptions exposes a string bodyLimit. Vext middleware receives the unified request/response objects, not Koa ctx.
Switch Adapter
Stop the current service, install the target peer dependency (npm install hono for Hono), then update src/config/default.ts:
Run npm run dev and the four requests above again: successful responses should now have data.adapter: "hono", with status, parameters, and validation results otherwise matching. Stop dev, rebuild, start production, and repeat so production cannot keep old output. Install other Adapters from the peer table and use the same checks.
Handlers and services based on VextRequest / VextResponse can generally be reused. Also test your application's case sensitivity, trailing slashes, query parameters, uploads, streams, cancellation, and errors. Four introductory requests do not prove a complete business migration.
How to choose Adapter
Select Native (recommended by default)
- Start with the framework's default path
- Do not require capabilities from another HTTP framework
- Build a new project without adapter migration constraints
- Keep additional dependencies to a minimum
Select Hono
- Your team knows Hono and wants its routing with the Web Request/Response bridge
- The deployment target is a supported Node.js environment
- Required behavior is available through Vext's public interface; native Hono middleware needs separate adaptation
Select Fastify
- You need Fastify routing or options actually exposed by this Adapter
- Your team has Fastify operations and debugging experience
- You have validated the business workload; native Fastify plugins and automatic serialization do not arrive merely by switching
Select Express
- Migrate existing Express projects to VextJS
- You can adapt native HTTP middleware to Vext middleware
- The team is most familiar with Express
Select Koa
- Your team knows Koa and
@koa/router - You can install both peers and have checked route matching
- You have an adaptation plan for native Koa middleware you need
VextAdapter interface
All Adapters implement the unified VextAdapter interface:
OpenAPI / Docs routes are registered by the framework through registerRoute(). Adapters no longer expose a separate registerOpenAPIRoutes() method.
Custom Adapter
Configuration accepts a built-in name, a synchronous factory (app: VextApp) => VextAdapter, or an already constructed Adapter. The factory receives the current app during initialization. The resolver checks name and required method presence; it does not prove correct middleware, error, or shutdown behavior.
If you only need to add behavior around an existing implementation, compose a built-in Adapter first. This runnable delegate preserves Native behavior while adding a name; it does not implement a different HTTP framework:
Replace only the existing adapter field in configuration, preserving other settings:
Repeat the dev/build/start and four requests above. Successful responses should report data.adapter: "my-custom". To integrate a genuinely different HTTP implementation, implement these contracts rather than leaving registration empty or returning 501 for every request:
- Convert input to
VextRequest, including route templates, params, raw body reads, and lifecycle signals; map output to the requiredVextResponse. - Preserve global and route-chain ordering, the return path of
await next(), error/404 handling, and suppliedRouteOptions. buildHandler()returns a Node.js request handler without listening, for dev handler replacement.listen()handles listen failures and server options and returns the actual port and an awaitableclose().- Verify normal/error/validation responses, headers and Cookies, uploads, streams, disconnects, and shutdown over real HTTP. Test both development and production startup. Framework-installed frontend rendering cannot be assumed complete from the interface shape alone.
Request/response conversion
Business code should use the unified interfaces with any Adapter. The following is a member summary, omitting full generics and internal response hooks; see Request and Response for precise public signatures and behavior. Do not copy this summary as a complete custom Adapter implementation.
VextRequest (unified request object)
_getRawBody() / _getRawBodyBuffer() are injected by adapters and primarily used by framework middleware and plugins such as multipart parsers. Application handlers should usually use req.body, req.files, and req.valid().
VextResponse (unified response object)
stream() / download() accept Node.js Readable / NodeJS.ReadableStream, not Web ReadableStream. rawJson() and underscore-prefixed response methods are framework internals; application code should use the public methods visible through VextPublicResponse.
This design means:
- Public interfaces give routes and middleware a reuse boundary.
- Every Adapter must implement the framework's request, response, and middleware contracts; native framework objects are outside that contract.
- Pure business unit tests may be reusable, while HTTP integration tests should run against the actual selected Adapter.
Switch Adapter according to environment
The configuration loader allows environment overrides. Use separate Adapters only when needed and verified in both environments; using the same one in development and production usually makes failures easier to reproduce. This example only shows the override mechanism; install Hono first:
FAQ
Do I need to modify the code after switching the Adapter?
Code using only public interfaces can generally be reused. Code reading native objects or relying on framework-specific plugins or route behavior needs adaptation and regression testing against the target Adapter. There is no guarantee that all business code is unchanged.
Can Adapter be switched dynamically at runtime?
Can't. Adapter is determined by configuration at startup and cannot be switched during runtime. If you need to use different Adapters depending on the environment, please use the configuration file override mechanism (such as development.ts / production.ts).
Where does the performance difference mainly come from?
Performance differences come from each framework's HTTP parsing, routing, and serialization and Vext's Adapter integration path. The public historical sample compares Adapters under the same Vext Normal workload and provides no overhead percentages against individual Raw baselines. Leading one scenario does not imply leading every scenario. Review the version and methodology in Performance benchmarks, then test your actual middleware and I/O workload.
Can the native middleware of the underlying framework be used?
Do not pass native middleware directly as Vext middleware. defineMiddleware / defineMiddlewareFactory use unified request, response, and next contracts with different signatures and lifecycles. Logic independent of native HTTP objects can be wrapped in Vext middleware or a plugin. Extensions depending on native instances need a bridge or custom Adapter; a thin function wrapper alone does not prove compatibility.
What should I do if peer dependencies report a warning?
Optional means you do not need peers for Adapters you do not select. The selected Adapter needs compatible peers: Hono needs hono, and Koa needs both koa and @koa/router. Distinguish an unused optional package from a missing selected peer or incompatible version rather than ignoring all installation warnings.
The current Hono Adapter is a Node.js runtime capability: it receives requests through a Node.js HTTP server and bridges them into Hono's Web Request / Response flow. Edge / Serverless runtimes should not use these Node adapter installation instructions as a support claim.
How do I diagnose startup or switching failures?
Cluster source IP affinity and custom adapters
All five built-in adapters share a Node HTTP receiver for cluster.sticky: "ip", retaining their routing, request, and shutdown lifecycles. Fastify uses its official serverFactory and continues through ready/listen/close.
The public VextAdapterFactory type is (app, context?: VextAdapterRuntimeContext) => VextAdapter. The second argument is per-instance runtime context. Only an actual Cluster Worker with affinity enabled receives context.socketHandoff; its host/port describe Master's bound public endpoint. A custom factory must not change runtime mode solely because the project config contains sticky.
Custom adapters keep their original required interface by default. To support this mode, declare supportsSocketHandoff: true, consume the factory context, and return a VextServerHandle with receiveSocket(socket) and synchronous forceClose() from listen(). The Worker binds neither a public nor a private TCP port in this mode. receiveSocket accepts a committed, paused socket, registers ownership, and then resumes reading. close waits for in-flight connections; forceClose releases every owned socket, including upgrades, within the framework's absolute shutdown deadline. Missing capability fails before listening; claiming support without returning the controls fails startup.
Existing one-argument factories remain valid in ordinary mode. Wrappers around built-in factories must forward context, for example (app, context) => nativeAdapter()(app, context). Injecting buildHandler() into an unlistened server does not by itself provide Node timeout, connection-counting, and shutdown contracts.
Next step
- Understand the Adapter-related configuration items in Configuration
- View the performance of OpenAPI Documentation under different Adapters
- Explore the cooperation between Cluster multi-process and Adapter
- Read benchmark data related to Performance Benchmark