Skip to content

vextjs/vext

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

173 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

VextJS

Documentation

VextJS is a high-performance full-stack Node.js framework for building maintainable applications. It combines a convention-based backend runtime, file-system routing, typed services, plugins, middleware, validation, OpenAPI generation, route-level caching, an esbuild-powered React frontend pipeline, and a CLI workflow that keeps projects productive from the first command.

Why VextJS

  • 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.
  • Runtime app.hooks for request, validation, response, error, fetch, service, cache, plugin, OpenAPI, and server lifecycle points.
  • Built-in request context, request id, access logging, body limit, structured error handling with app.throw details, i18n, and OpenAPI endpoints.
  • Built-in app.fetch with timeout/retry/requestId propagation and config-driven app.fetch.proxy response passthrough.
  • Route-level response cache powered by response-cache-kit / cache-hub, with memory, Redis, and multi-level modes.
  • Built-in React frontend integration for src/frontend/pages/**, route-driven res.render(), SSR, hydration telemetry, route-specific modulepreload, Vext JSCSS/CSS assets, scoped SPA fallback, and generated API contract files.
  • Lightweight vextjs/frontend runtime helpers for page i18n, generated API contract artifacts, and future external frontend adapters.
  • 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.

Quick Start

npx vextjs create my-app
cd my-app
npm run dev

Open http://localhost:3000. The default scaffold is a full-stack React app rendered from Vext routes. It includes a React page, a shared layout, default error page, /api/hello, and /api/health so the project is runnable immediately.

Create a project with another adapter:

npx vextjs create my-app --adapter hono

Create a JavaScript project:

npx vextjs create my-app --js

Create an API-only project:

npx vextjs create my-api --template api --frontend none

Skip dependency installation:

npx vextjs create my-app --skip-install

Installation

Manual setup is also supported:

npm install vextjs

package.json:

{
  "name": "my-app",
  "type": "module",
  "scripts": {
    "dev": "vext dev",
    "build": "vext build",
    "start": "vext start"
  },
  "dependencies": {
    "vextjs": "^0.3.26"
  }
}

VextJS projects use ESM. Keep "type": "module" in application packages.

Project Structure

The scaffold creates the convention directories that the runtime knows how to scan:

my-app/
|-- preload/                    # Optional process-level preload scripts
|   `-- README.md
|-- public/
|   `-- favicon.svg             # Static assets copied into the frontend build
|-- src/
|   |-- frontend/
|   |   |-- pages/
|   |   |   |-- _document.html   # HTML document with {vext.*} slots
|   |   |   |-- index.tsx        # Page rendered by res.render("index")
|   |   |   |-- layout.tsx       # Shared layout chain entry
|   |   |   `-- error/
|   |   |       `-- default.tsx  # Default HTML error page
|   |   |-- components/
|   |   |   `-- AppShell.tsx
|   |   |-- locales/
|   |   |   `-- en-US.ts
|   |   |-- styles/
|   |   |   |-- index.css
|   |   |   `-- card.style.ts
|   |   `-- assets/
|   |-- 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        # vext typegen writes index.d.ts here
|-- package.json
`-- tsconfig.json

JavaScript projects use .js files and do not create src/types/generated/. Generated TypeScript declarations are stored under .vext/types/; src/types/generated/index.d.ts is a small reference shim created by vext typegen. Frontend generated source lives under .vext/generated/frontend/; browser and SSR output is written under .vext/client/ in development and dist/client/ during production build.

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.ts

src/config/local.ts and src/config/local.js are ignored by the generated .gitignore because they may reference private local infrastructure.

CLI

vext dev              # Development mode with hot reload
vext build            # Build TypeScript projects
vext start            # Start the production server from dist/
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 status

vext dev prints a minimal ready log by default: listening URL(s) plus total startup time. Add --startup-profile to print startup timings grouped by stable phases such as main/preflight, main/preload, pre-worker-bootstrap, compile, database, plugins, routes, openapi, listen, and onReady. Use --startup-profile-json .vext/inspect/startup-profile.json to write the same phase names and gap.* events to JSON without enabling human-readable profile details.

vext start keeps production output minimal too: start mode, listening URL(s), and total startup time. Add --startup-profile or --startup-profile-json <path> when you need cold-start phase timings from the production bootstrap path.

vext 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-api --template api --frontend none
vext create my-app --skip-install
vext create my-app --force

Configuration

Configuration is loaded and merged in this order:

framework defaults -> default -> config profile -> 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,
    prettyColor: "auto",
  },
  server: {
    requestTimeout: 120_000,
    headersTimeout: 60_000,
    keepAliveTimeout: 5_000,
  },
  openapi: {
    enabled: true,
  },
  frontend: {
    enabled: true,
    framework: "react",
    publicDir: "public",
    publicPath: "/",
    i18n: {
      enabled: true,
      defaultLocale: "en-US",
    },
    dev: {
      renderRefresh: "prompt",
    },
  },
};

export default config;

Config profile files can return partial config. vext start, vext build, and vext deploy assets default to the production profile; vext dev defaults to development. Use --config <name> or VEXT_CONFIG=<name> to load a custom profile such as src/config/sg-sit.ts:

vext start --config sg-sit
VEXT_CONFIG=sg-sit vext start
vext deploy assets --config sg-sit --dry-run

Profile 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.

app.logger uses Vext's built-in structured logger by default. It outputs JSON in production, uses an internal pretty formatter in development, colors pretty level labels in TTY terminals or with FORCE_COLOR=1 through logger.prettyColor: "auto", supports trace(), runtime getLevel() / setLevel(), and exact key/path redaction through logger.redactKeys / logger.redactPaths. JSON output never contains ANSI color codes. Plugins can wrap it through app.setLogger() for external log bridges.

Use config.server for inbound Node.js HTTP server settings such as request, headers, keep-alive, socket timeout, request header size, max requests per socket, and incomplete-request checking interval. It applies to the built-in Native, Hono, Fastify, Express, Koa adapters and the dev server; omitted fields keep the current Node.js defaults. This is separate from config.fetch.timeout, which only controls outbound app.fetch calls.

Frontend

The default vext create template enables config.frontend and creates src/frontend/**. URL entry still lives in src/routes/**; a route renders a page by calling res.render(page, props, options):

app.get("/", {}, async (req, res) => {
  const greeting = await app.services.example.greeting("Vext");
  res.render("index", { greeting });
});

vext dev builds the browser app into .vext/client/, watches src/frontend/** and public/**, and uses the dev event bus for CSS/JSCSS updates, React Fast Refresh, and optional render-data refresh prompts after backend soft reloads.

Component styles can use the default vextjs/style facade. Files such as src/frontend/styles/card.style.ts are scanned during build, extracted into generated CSS, and merged into the final CSS asset without adding Emotion or styled-components as default runtime dependencies.

vext build compiles server code and then bundles the browser client and SSR renderer into dist/client/. Browser pages, layouts, error pages, and locale entries are split through dynamic imports; shared React runtime packages go through the Vext-managed vendor entry. vext start serves static frontend assets and HTML rendering while leaving API paths such as /api/**, /openapi.json, and /docs/** to the backend runtime.

Production builds also write dist/client/deploy-manifest.json for CDN or static asset publishing and dist/client/size-report.json for raw/gzip/brotli size evidence, route initial assets, and app-owned/external runtime groups. vext deploy assets and vext build --upload-assets upload only changed JS/CSS/images/fonts and copied public/** files by sha256 state; server-rendered index.html is not uploaded by default.

For React hydration performance budgets, set fields such as frontend.build.budgets.maxInitialJsBrotliBytes, maxRouteInitialJsBrotliBytes, or maxAppOwnedInitialJsBrotliBytes. The default frontend i18n mode uses frontend.i18n.clientLoad: "current" so the browser loads only the SSR locale during hydration; use "all" only when a page needs no-reload locale switching. Advanced React CDN/import-map usage remains opt-in through frontend.build.client.external plus externalRuntime; React-related externals must provide runtime mappings.

For the frontend user guide, start with Frontend Overview. The Frontend section has its own navigation for getting started, routing/pages, SSR, hydration, CSR fallback, render data cache, Fast Refresh, code splitting, static assets/CDN, performance budgets, configuration, and troubleshooting.

When frontend.apiClient is enabled, Vext also emits client-contract.json and api.generated.ts next to the frontend output for tooling or advanced external frontend integrations. Normal page code does not need to hand-write route contracts; for first-screen data, prepare services in the route handler and pass props through res.render().

For API-only projects, use:

npx vextjs create my-api --template api --frontend none

Startup Config Providers

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({ configProfile, signal }) {
        const response = await fetch(
          `https://config.example.com/${configProfile}.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.

Preload

VextJS supports two preload sources:

  • Application-level scripts in the project root preload/ directory.
  • Package-level scripts declared through package.json vext.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

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() });
    },
  );
});

Validation

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/.

Error Handling

VextJS catches exceptions thrown from routes, services, and middleware through a built-in global error-handler.

  • Use app.throw(...) when you want to return a structured HTTP error such as 404, 409, or a custom business code.
  • Throw new VextValidationError(errors) when you want to return a 422 response with field-level validation details.
  • Throw new Error("...") for unexpected runtime failures. VextJS will convert it to a 500 Internal Server Error.

app.throw also supports optional business details for cases such as upstream API errors:

app.throw(
  502,
  "payment.failed",
  { orderId },
  {
    provider: "stripe",
    providerCode: "card_declined",
  },
);

app.throw({
  status: 502,
  message: "payment.failed",
  code: "PAYMENT_FAILED",
  details: { provider: "stripe", providerCode: "card_declined" },
});

details is sanitized before it is written to the JSON response, so circular references and unsupported values cannot break error serialization.

See the full guide in Error Handling and the App API.

For unexpected runtime errors, detailed stack traces are intended for development and diagnostics:

  • In development, you can expose stack in JSON by setting response.hideInternalErrors = false.
  • Browser requests in dev mode can also render the built-in HTML error overlay with stack frames and source context.
  • In production, keep hideInternalErrors enabled so clients receive a safe 500 response instead of internal details.

Services

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 typegen

Generated declarations are written to .vext/types/, with src/types/generated/index.d.ts referencing them for TypeScript projects.

Middleware

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

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";
      },
    });
  },
});

For precise app extension typing, export appExtensions = defineAppExtensions<{ ... }>() with an inline object generic from the plugin file. Legacy app.extend() calls are still scanned automatically as a best-effort fallback. After adding app extensions, run vext typegen so TypeScript consumers see the new fields.

Runtime Hooks

Use app.hooks.on(name, handler) to observe or patch framework lifecycle points without replacing core middleware:

app.hooks.on("validation:success", ({ req, route }) => {
  app.logger.info({ requestId: req.requestId, route: route.path }, "validated");
});

app.hooks.on("response:before", ({ headers }) => ({
  headers: { ...headers, "x-powered-by": "vext" },
}));

app.hooks.on("service:beforeCall", ({ service, method }) => {
  app.logger.debug({ service, method }, "service call");
});

Available lifecycle families include request/route, validation, handler, response, error, fetch/proxy, service, cache, plugin, routes, OpenAPI, server, ready, and close. app.hooks is a reserved app property and cannot be overwritten with app.extend("hooks", ...).

See the full guide in Runtime Hooks and the App API.

Adapters

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/router@^15.6.0

Response Cache

Response 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 runtime delegates response caching to response-cache-kit, backed by cache-hub. Vext captures successful JSON responses from GET or HEAD routes, stores them with millisecond TTLs, and serves later hits before validation and handler execution. Cache keys can be static strings or request-based functions; use partitionKey for user or tenant isolation.

Configure the runtime in config.cache. The legacy Memory shorthand still works:

export default {
  cache: {
    defaultTtl: 60_000,
    maxEntries: 1000,
    maxMemory: 50 * 1024 * 1024,
  },
};

For Redis or multi-level response cache, use the cacheHub runtime config:

export default {
  cache: {
    defaultTtl: 2_000,
    cacheHub: {
      mode: "redis",
      url: "redis://localhost:6379",
      lease: { waitForOwner: 1_000, onTimeout: "fetch" },
      distributed: { channel: "vext:response-cache" },
    },
  },
};

OpenAPI

Enable OpenAPI in config:

export default {
  openapi: {
    enabled: true,
    title: "My API",
    version: "1.0.0",
  },
};

Then visit:

  • http://localhost:3000/docs
  • http://localhost:3000/openapi.json

Route metadata is collected from docs, validation declarations, parameters, responses, and route registration data.

i18n

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.

Development Hot Reload

vext dev chooses the smallest safe reload strategy:

Change type Strategy
Route files Hot route replacement
Service or locale files Service/i18n reload
Frontend files or public assets Frontend rebuild
Config, plugin, preload, env, or package files Cold restart

TypeScript projects are compiled into .vext/dev/ during development.

Build And Start

npm run build
npm start

vext build refreshes generated types and manifest files before compiling TypeScript source and project-level preload files. When frontend.enabled is true, it also bundles the browser client and writes dist/client/manifest.json, dist/client/render-manifest.json, dist/client/deploy-manifest.json, dist/client/size-report.json, dist/client/index.html, and API contract artifacts. The render manifest includes route initial assets used by SSR modulepreload; production vext start fails fast when that schema is missing and asks you to rebuild instead of serving stale frontend output. vext start runs the production bootstrap path, can read compiled preload files from dist/preload/, and serves the frontend build when present.

For TypeScript projects, run vext build before vext start. Development should use vext dev; production start does not fall back to a TypeScript runtime.

Testing Utilities

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

Requirements

  • Node.js >=20.19.0
  • ESM application packages

License

Apache-2.0

About

VextJS is a high‑performance Node.js framework that integrates scaffolding, modular architecture, and a plugin‑based runtime, enabling teams to build maintainable and scalable backend systems with exceptional development efficiency.

Topics

Resources

License

Contributing

Stars

Watchers

Forks

Packages

 
 
 

Contributors