VextJS is a high-performance Node.js framework for building maintainable backend services. It combines a convention-based project structure, file-system routing, typed services, plugins, middleware, validation, OpenAPI generation, route-level caching, and a CLI workflow that keeps projects productive from the first command.
- Convention-based structure for routes, services, middleware, plugins, config, locales, generated types, and preload scripts.
- File-system routing with dynamic params, nested routes, validation, middleware, OpenAPI metadata, and response helpers.
- Adapter support for Native Node.js, Hono, Fastify, Express, and Koa.
- Automatic service injection through
app.services. - Plugin lifecycle hooks with app extension support.
- Built-in request context, request id, access logging, body limit, error handling, i18n, and OpenAPI endpoints.
- Route-level response cache with LRU memory storage.
- Hot development workflow with route hot swap, service/i18n reload, and cold restart only when required.
- Type generation for service and plugin app extensions.
- Process-level preload support for OpenTelemetry, APM, polyfills, and startup bridges.
npx vextjs create my-app
cd my-app
npm run devOpen http://localhost:3000. The scaffold includes a root route and a health check so the project is runnable immediately.
Create a project with another adapter:
npx vextjs create my-app --adapter honoCreate a JavaScript project:
npx vextjs create my-app --jsSkip dependency installation:
npx vextjs create my-app --skip-installManual setup is also supported:
npm install vextjspackage.json:
{
"name": "my-app",
"type": "module",
"scripts": {
"dev": "vext dev",
"build": "vext build",
"start": "vext start"
},
"dependencies": {
"vextjs": "^0.3.12"
}
}VextJS projects use ESM. Keep "type": "module" in application packages.
The scaffold creates the convention directories that the runtime knows how to scan:
my-app/
|-- preload/ # Optional process-level preload scripts
| `-- README.md
|-- src/
| |-- config/
| | |-- default.ts # Required base config
| | |-- development.ts # Development override
| | |-- production.ts # Production override
| | |-- local.example.ts # Copy to local.ts for private local overrides
| | `-- bootstrap.example.ts # Copy to bootstrap.ts for startup providers
| |-- routes/
| | `-- index.ts
| |-- services/
| | `-- example.ts
| |-- middlewares/
| | `-- README.md
| |-- plugins/
| | `-- README.md
| |-- locales/
| | `-- README.md
| `-- types/
| `-- generated/
| `-- .gitkeep
|-- package.json
`-- tsconfig.json
JavaScript projects use .js files and do not create src/types/generated/.
local.example.ts and bootstrap.example.ts are examples, not active config files. Copy them when you need the feature:
cp src/config/local.example.ts src/config/local.ts
cp src/config/bootstrap.example.ts src/config/bootstrap.tssrc/config/local.ts and src/config/local.js are ignored by the generated .gitignore because they may reference private local infrastructure.
vext dev # Development mode with hot reload
vext build # Build TypeScript projects
vext start # Start the production server
vext create <name> # Create a new project
vext typegen # Generate service and app extension types
vext stop # Stop cluster workers
vext reload # Rolling restart for cluster workers
vext status # Inspect cluster statusvext create options:
vext create my-app
vext create my-app --js
vext create my-app --adapter hono
vext create my-app --adapter fastify
vext create my-app --adapter express
vext create my-app --adapter koa
vext create my-app --adapter native
vext create my-app --skip-install
vext create my-app --forceConfiguration is loaded and merged in this order:
framework defaults -> default -> NODE_ENV file -> local -> bootstrap provider patch -> CLI override
src/config/default.ts:
import type { VextUserConfig } from "vextjs";
const config: VextUserConfig = {
port: 3000,
adapter: "native",
logger: {
level: "info",
pretty: true,
},
openapi: {
enabled: true,
},
};
export default config;Environment files can return partial config:
// src/config/production.ts
import type { VextUserConfig } from "vextjs";
const config: Partial<VextUserConfig> = {
port: 3001,
logger: {
level: "info",
pretty: false,
},
};
export default config;Use src/config/local.ts for machine-specific overrides and keep it out of Git.
Use src/config/bootstrap.ts when configuration must be fetched before the final app config is validated and frozen:
import { defineBootstrapConfig } from "vextjs";
export default defineBootstrapConfig({
providers: [
{
name: "remote-config",
async load({ env, signal }) {
const response = await fetch(`https://config.example.com/${env}.json`, {
signal,
});
return await response.json();
},
},
],
});This is the right place for startup config centers and early infrastructure patches. Use preload/ instead for APM, OpenTelemetry, polyfills, or anything that must execute before application modules are imported.
VextJS supports two preload sources:
- Application-level scripts in the project root
preload/directory. - Package-level scripts declared through
package.jsonvext.preload.
Application preload example:
preload/
|-- 01-otel.ts
`-- 02-polyfill.mjs
Supported application preload files include .js, .mjs, .ts, and .mts. TypeScript preload files are compiled before injection. vext dev watches the root preload/ directory and performs a cold restart when preload files change.
Routes live in src/routes/ and are mapped from file paths to URL prefixes:
src/routes/index.ts -> /
src/routes/users.ts -> /users
src/routes/admin/index.ts -> /admin
src/routes/admin/settings.ts -> /admin/settings
src/routes/users/[id].ts -> /users/:id
Example:
import { defineRoutes } from "vextjs";
export default defineRoutes((app) => {
app.get(
"/",
{
docs: { summary: "Home" },
},
async (_req, res) => {
const greeting = await app.services.example.greeting("Vext");
res.json(greeting);
},
);
app.get(
"/health",
{
docs: { summary: "Health check" },
},
async (_req, res) => {
res.json({ status: "ok", timestamp: Date.now() });
},
);
});Route validation uses schema-dsl style declarations:
app.post(
"/users",
{
validate: {
body: {
name: "string!",
age: "number|min:0",
email: "email!",
},
},
},
async (req, res) => {
const body = req.valid("body");
res.json({ created: true, user: body });
},
);Validation errors use HTTP 422 by default and can be localized through src/locales/.
Services live in src/services/ and are injected into app.services by filename:
// src/services/example.ts
import type { VextApp } from "vextjs";
export default class ExampleService {
constructor(private app: VextApp) {}
async greeting(name: string) {
this.app.logger.info("Generating greeting", { name });
return { message: `Hello, ${name}! Welcome to VextJS.` };
}
}Use it from a route:
const result = await app.services.example.greeting("Vext");Run type generation after changing services or app extensions:
npx vext typegenGenerated declarations are written to src/types/generated/.
Middleware files live in src/middlewares/ and are referenced by name from route config or global configuration.
// src/middlewares/auth.ts
import { defineMiddleware } from "vextjs";
export default defineMiddleware(async (req, res, next) => {
if (!req.headers.get("authorization")) {
return res.status(401).json({ error: "Unauthorized" });
}
return next();
});Plugins live in src/plugins/ and can register lifecycle hooks, resources, and app extensions:
import { definePlugin } from "vextjs";
export default definePlugin({
name: "redis",
async setup(app) {
app.extend("redis", {
async ping() {
return "PONG";
},
});
},
});After adding app extensions, run vext typegen so TypeScript consumers see the new fields.
The default adapter is Native Node.js:
const config = {
adapter: "native",
};Other adapters are available through package subpaths:
import { honoAdapter } from "vextjs/adapters/hono";
export default {
adapter: honoAdapter(),
};Install the matching peer dependency before using a non-native adapter:
npm install hono @hono/node-server
npm install fastify
npm install express
npm install koa @koa/routerRoute cache is enabled at route level:
app.get(
"/articles",
{
cache: {
ttl: 60_000,
key: "articles:list",
},
},
async (_req, res) => {
res.json(await app.services.article.list());
},
);The current runtime uses MemoryCacheStore, an in-process LRU store. Cached GET or HEAD responses are written after a successful handler response and served by generated route cache middleware on later requests. Cache keys can be static strings or request-based functions.
Enable OpenAPI in config:
export default {
openapi: {
enabled: true,
title: "My API",
version: "1.0.0",
},
};Then visit:
http://localhost:3000/docshttp://localhost:3000/openapi.json
Route metadata is collected from docs, validation declarations, parameters, responses, and route registration data.
Put locale files in src/locales/:
// src/locales/en-US.ts
export default {
validation: {
required: "This field is required.",
},
};The runtime automatically loads locale files during bootstrap. In development, locale changes trigger the service/i18n reload path.
vext dev chooses the smallest safe reload strategy:
| Change type | Strategy |
|---|---|
| Route files | Hot route replacement |
| Service or locale files | Service/i18n reload |
| Config, plugin, preload, env, or package files | Cold restart |
TypeScript projects are compiled into .vext/dev/ during development.
npm run build
npm startvext build compiles TypeScript source and project-level preload files. vext start runs the production bootstrap path and can read compiled preload files from dist/preload/ when the root preload/ directory is not present.
VextJS exports testing helpers through vextjs/testing:
import { createTestApp } from "vextjs/testing";Use the testing entry for integration tests that need the framework runtime without starting a real production process.
- Documentation site: https://vextjs.github.io/vext/
- Changelog: CHANGELOG.md
- Detailed release notes: changelogs/
- Issues: https://github.com/vextjs/vext/issues
- Node.js
>=18.0.0 - ESM application packages
MIT