Configuration

VextJS uses a multi-layer configuration merging mechanism to support configuration overrides by environment, while providing a rich set of built-in configuration items to cover framework behaviors.

Configuration loading mechanism

When the framework starts, config-loader loads configuration files and merges them deeply in the following order:

Framework built-in defaults → default.ts → {configProfile}.ts → local.ts → bootstrap provider patch → CLI override

Each layer can declare only the fields that need to be covered, and undeclared fields are inherited from the previous layer.

Configuration file

FilePurposeIs it necessary
src/config/default.tsBasic configuration for all environments✅ Required
src/config/development.tsDevelopment profile override (vext dev default)Optional
src/config/production.tsProduction profile override (vext start default)Optional
src/config/test.tsTest profile overrideOptional
src/config/local.tsLocal development coverage (should be added to .gitignore)Optional
src/config/bootstrap.tsStartup provider registration entranceOptional

Select a config profile explicitly with --config <name> or VEXT_CONFIG=<name>. When omitted, vext start, vext build, and vext deploy assets default to the production profile, while vext dev defaults to the development profile.

Profile names can represent custom deployment environments, for example:

  • src/config/sg-sit.ts
  • src/config/us-uat.ts
  • src/config/us-prod.ts

Pass the profile name at startup:

vext start --config sg-sit
VEXT_CONFIG=sg-sit vext start

Vext will be loaded according to the same set of merge links: default -> sg-sit -> local -> bootstrap provider patch -> CLI override.

Build, Runtime, and Config Profile semantics

vext build statically injects process.env.NODE_ENV in user source code as "production", and vext start runs with production runtime mode. Config profile selection is independent and is controlled by --config / VEXT_CONFIG.

Therefore, it is recommended to put the environmental differences into:

  • src/config/<env>.ts
  • src/config/bootstrap.ts
  • Other explicit business environment variables

Instead of relying on the process.env.NODE_ENV conditional branch in the source code after build.

Merge rules

  • Object fields: deep merge, the environment file only needs to declare the fields that need to be covered
  • middlewares array: smart patch strategy - match and merge by name instead of simply replacing the entire array
  • Other Arrays: The back layer covers the front layer
  • bootstrap provider patch: Participate in the same merge / validate / freeze process after local.ts and before CLI override
  • Final result: deep freeze (deepFreeze), unmodifiable at runtime

Bootstrap Config Provider

If you need to pull the remote configuration (such as Nacos/Configuration Center/Startup Key Distribution) before finalizing the configuration, you can add src/config/bootstrap.ts:

import { defineBootstrapConfig } from "vextjs";

export default defineBootstrapConfig({
  providers: [
    {
      name: "remote-config",
      timeoutMs: 10_000,
      async load({ configProfile, baseConfig, signal }) {
        const response = await fetch(
          `https://config.example.com/${configProfile}`,
          { signal },
        );

        const remote = await response.json();
        return {
          database: remote.database,
          logger: {
            lifecycleLevel: baseConfig.logger?.lifecycleLevel ?? "concise",
          },
        };
      },
    },
  ],
});

provider context field:

FieldDescription
envCurrent environment (such as development / production / test)
baseConfigdefault/env/local Merged read-only configuration, which can be used to determine patch based on existing configuration
signalAbortSignal that aborts on timeout or cancellation
rootDir / configDirCurrent project and configuration directory path
command / isBuiltThe current startup command and whether to compile the product

Constraints:

  • provider must return plain object patch or null
  • patch only supports JSON-like structure; does not support functions, class instances, and adapter factory
  • Default priority: local < provider < CLI
  • When required is not declared: production defaults to fail-fast, development/test defaults to continue after warning
  • In Cluster mode, the Master will pass the current round of provider patches to the Worker for reuse to avoid configuration drift in the same startup cycle.

Configuration file format

Export one object per configuration file using export default:

// src/config/default.ts
export default {
  port: 3000,
  host: "0.0.0.0",
  logger: {
    level: "info",
    lifecycleLevel: "concise",
  },
  cors: {
    origins: ["*"],
  },
  openapi: {
    enabled: true,
  },
};
// src/config/production.ts — only overwrite the fields that need to be changed
export default {
  logger: {
    level: "warn", //Reduce log output in production environment
  },
  cors: {
    origins: ["https://myapp.com"], // Production environment restricted origins
  },
  openapi: {
    enabled: false, // Close the document in production environment
  },
};
// src/config/local.ts — special configuration for local development (do not submit to Git)
export default {
  port: 8080, // Use other ports locally
};

Middlewares Patch Strategy

The middlewares array uses smart merging, matching by middleware name:

// src/config/default.ts
export default {
  middlewares: [
    "auth",
    { name: "check-role", options: { roles: ["user"] } },
    { name: "rate-limit-api", options: { max: 100 } },
  ],
};
// src/config/development.ts
export default {
  middlewares: [
    // Just declare the middleware to be overridden and leave the rest
    { name: "check-role", options: { roles: [] } }, // The development environment does not check roles
    { name: "rate-limit-api", options: { max: 10000 } }, // Relax the rate limit
  ],
};

Merged result:

middlewares: [
  "auth", // reserved
  { name: "check-role", options: { roles: [] } }, // overridden
  { name: "rate-limit-api", options: { max: 10000 } }, // overridden
];

Use Adapter

Native Adapter (http.createServer + route-core) is used by default. To switch to another Adapter, specify the adapter field in the configuration:

// src/config/default.ts — using Hono Adapter
import { honoAdapter } from "vextjs/adapters/hono";

export default {
  adapter: honoAdapter(),
  port: 3000,
};
// src/config/default.ts — using Fastify Adapter
import { fastifyAdapter } from "vextjs/adapters/fastify";

export default {
  adapter: fastifyAdapter(),
  port: 3000,
};
// src/config/default.ts — using Express Adapter
import { expressAdapter } from "vextjs/adapters/express";

export default {
  adapter: expressAdapter(),
  port: 3000,
};
// src/config/default.ts — using Koa Adapter
import { koaAdapter } from "vextjs/adapters/koa";

export default {
  adapter: koaAdapter(),
  port: 3000,
};
Tip

When adapter is not specified, Native Adapter is used by default, which has the highest performance and zero framework dependency. Only switch when you need to use the ecology or features of a specific framework.

Frontend configuration (frontend)

frontend controls the built-in browser pipeline. It can be true, false, or an object:

export default {
  frontend: {
    enabled: true,
    framework: "react",
    root: "src/frontend",
    publicDir: "public",
    publicPath: "/",
    styles: {
      jscss: {
        enabled: true,
      },
    },
    i18n: {
      enabled: true,
      defaultLocale: "en-US",
    },
    spaFallback: {
      scopes: [
        {
          basePath: "/admin/app",
          page: "admin/app/shell",
          exclude: ["/admin/api/**"],
        },
      ],
    },
    apiClient: {
      enabled: true,
    },
    build: {
      target: "es2022",
      minify: true,
      sourcemap: false,
      client: {
        external: [],
        externalRuntime: {},
      },
      vendorChunks: {
        enabled: true,
      },
      budgets: {
        maxTotalBytes: 5_000_000,
      },
    },
    deploy: {
      integrity: true,
      upload: {
        enabled: false,
        adapter: "filesystem",
        targetDir: ".vext/deploy/frontend-assets",
        prefix: "my-app",
        exclude: ["**/*.map"],
      },
    },
  },
};
Configuration itemTypeDefault valueDescription
frontend.enabledbooleanfalseEnable built-in frontend integration
frontend.frameworkstring'react'Frontend framework label
frontend.rootstring'src/frontend'Frontend source directory
frontend.entrystring'.vext/generated/frontend/browser-entry.tsx'Generated browser entry; usually not written by hand
frontend.indexHtmlstring'src/frontend/pages/_document.html'HTML document template
frontend.outDirstring.vext/client in dev, dist/client in productionFrontend output directory
frontend.styles.jscssboolean | object{ enabled: true }Vext JSCSS build-time CSS extraction and dynamic CSS variables
frontend.publicDirstring'public'Static asset directory copied into output and deploy manifest
frontend.publicPathstring'/'Public asset path prefix
frontend.spaFallbackboolean | object{ scopes: [] }Serve fallback only for explicitly declared client-router sub-app scopes
frontend.apiClientboolean | objecttrueGenerate client contract artifacts
frontend.build.targetstring | string[]'es2022'Browser build target
frontend.build.minifybooleanProduction trueMinify frontend output
frontend.build.sourcemapbooleanDevelopment trueGenerate frontend source maps
frontend.build.client.externalstring[][]Browser bundle external modules
frontend.build.client.externalRuntimeobject{}Import map URL mapping for externalized browser modules
frontend.build.vendorChunksboolean | object{ enabled: true }Shared dependency chunk management
frontend.build.budgetsobjectAll 0Build size budgets; 0 disables a budget
frontend.build.assets.inlineLimitnumber0Imported image/font inline limit
frontend.build.css.modulesbooleantrueSupports the .module.css CSS Modules convention
frontend.deploy.assetBaseUrlstringNoneAbsolute CDN prefix for frontend static assets
frontend.deploy.integritybooleanfalseInject SRI integrity for JS/CSS tags
frontend.deploy.uploadboolean | object{ enabled: false, exclude: ["**/*.map"] }Static asset upload and incremental deployment configuration

By default spaFallback.scopes is empty, so unknown HTML paths are not swallowed into the SPA. For mixed SSR + client-router sub-apps, declare each basePath in scopes[]. spaFallback: true is kept only as a compatibility shorthand and is not recommended for enterprise mixed projects.

When frontend.deploy.upload is enabled, vext deploy assets reads dist/client/deploy-manifest.json and uploads changed assets by uploadKey and sha256. The built-in filesystem adapter writes files to targetDir, which is useful as a CDN sync staging directory. HTML is still rendered by Vext, and index.html plus **/*.map are excluded from the default deploy manifest.

For creating the app, changing pages, adding components, CSS/JSCSS, assets, API calls, HTML templates, and troubleshooting, see the Frontend guide.

Complete configuration item reference

Basic configuration

Configuration itemTypeDefault valueDescription
portnumber3000HTTP listening port
hoststring'0.0.0.0'HTTP listening address
adapterstring | Function | VextAdapter'native'Low-level adapter
trustProxybooleanfalseWhether to trust the proxy (affects req.ip / req.protocol)
frontendboolean | object{ enabled: false }Built-in frontend build and static serving configuration
export default {
  port: 3000,
  host: "0.0.0.0",
  trustProxy: false,
};

CORS configuration (cors)

Configuration itemTypeDefault valueDescription
cors.enabledbooleantrueWhether to enable CORS middleware
cors.originsstring[]['*']List of allowed origins
cors.methodsstring[]['GET','POST','PUT','PATCH','DELETE','HEAD','OPTIONS']Allowed HTTP methods
cors.headersstring[]['Content-Type','Authorization','X-Request-Id']Allowed request headers
cors.credentialsbooleanfalseWhether to allow carrying credentials
export default {
  cors: {
    origins: ["https://myapp.com"], // Production environment restriction origins (array format)
    credentials: true,
    methods: ["GET", "POST", "PUT", "DELETE"],
  },
};

Rate limiting configuration (rateLimit)

Configuration itemTypeDefault valueDescription
rateLimit.enabledbooleantrueWhether to enable global throttling
rateLimit.maxnumber100Maximum number of requests within the time window
rateLimit.windownumber60Time window (seconds)
rateLimit.messagestring'Too many requests'Rate limiting response message
rateLimit.keyBystring'ip'Rate limit dimension ('ip' / custom field)
export default {
  rateLimit: {
    enabled: true,
    max: 100,
    window: 60, // 1 minute (unit: seconds)
    message: "Too many requests, please try again later",
    keyBy: "ip",
  },
};
Route-level current limiting coverage

You can override the rate limiting configuration for a specific route in the route's options.override.rateLimit:

app.post(
  "/login",
  {
    override: {
      rateLimit: { max: 5, window: 60 }, // The login interface is more strict (window unit: seconds)
    },
  },
  handler,
);

app.get(
  "/health",
  {
    override: {
      rateLimit: false, // Health check does not limit the flow
    },
  },
  handler,
);

Request ID configuration (requestId)

Configuration itemTypeDefault valueDescription
requestId.enabledbooleantrueWhether to enable request ID
requestId.headerstring'x-request-id'Request ID transparent transmission header name
requestId.generate() => stringcrypto.randomUUIDCustom ID generation function
export default {
  requestId: {
    enabled: true,
    header: "x-request-id",
  },
};

When the request carries the X-Request-Id header, the framework will transparently transmit the ID instead of generating a new one. Suitable for microservice link tracking.

Log configuration (logger)

Configuration itemTypeDefault valueDescription
logger.levelstring'info'Log level
logger.lifecycleLevel'concise' | 'verbose''concise'Framework life cycle log detail level: startup, loader, hot reload, cluster and other system logs
logger.prettybooleanDevelopment environment trueWhether to use the built-in pretty formatter to output a readable format; the production environment is turned off by default (output JSON)
logger.prettyColor'auto' | 'always' | 'never''auto'Whether to add ANSI to the level label in pretty mode; the production JSON does not contain ANSI
logger.prettySingleLinebooleantrueIn pretty mode, compress extra fields in JSON inline form into the same line of the message; false uses multi-line expansion format
logger.prettyIgnorestring'pid,hostname,requestId'Fields to ignore in pretty mode (comma separated); requestId is hidden by default to avoid mixin injected fields from expanding into multi-line noise
logger.redactKeysstring[][]Desensitize structured log fields by exact key at any level
logger.redactPathsstring[][]Desensitize structured log fields by dot notation exact path
logger.redactValuestring`'[Redacted]''Desensitized replacement value
logger.mixinfunctionundefinedSynchronously return custom structured fields; requestId cannot be overridden, trace_id / span_id can be overridden by user fields

Supported log levels (from low to high): 'trace''debug''info''warn''error''fatal''silent'

export default {
  logger: {
    level: "info", // Recommended for production environment 'warn'
    lifecycleLevel: "concise", // If you need to troubleshoot, set it to 'verbose'
    pretty: true, // Enable readable formatting in the development environment (disabled by default in the production environment)
    // prettyColor: 'auto', // Add color to level label when TTY or FORCE_COLOR=1
    // prettySingleLine: true, // Extra fields are compressed to the same line (default)
    // prettyIgnore: 'pid,hostname,requestId', // Hidden fields by default
    // redactKeys: ['password', 'token'], // exact key desensitization
    // redactPaths: ['headers.authorization'], // exact path desensitization
    // mixin: () => ({ service_name: 'my-app' }), // Custom structured fields
  },
};

VextJS has a built-in logger kernel with zero runtime dependency, and the pretty mode uses the built-in formatter to output readable logs. The default logger supports trace(), getLevel() / setLevel() and exact key/path redaction; see Log Document for complete description.

Graceful shutdown configuration (shutdown)

Configuration itemTypeDefault valueDescription
shutdown.timeoutnumber10Shutdown timeout (seconds), force exit after timeout
export default {
  shutdown: {
    timeout: 15, // 15 seconds timeout (unit: seconds)
  },
};

After receiving the SIGTERM / SIGINT signal, the framework executes all onClose hooks (such as closing the database connection) in the reverse order of registration, and forcefully exits after timeout.

HTTP Server Configuration (server)

server controls the inbound Node.js HTTP server layer behavior, applicable to the built-in Native / Hono / Fastify / Express / Koa adapter, and also applicable to the development server created by vext dev. Unconfigured fields retain their current Node.js default values.Configuration itemTypeDefault valueDescription
server.requestTimeoutnumberNode.js defaultMaximum time in milliseconds to receive a complete request, 0 means disabled
server.headersTimeoutnumberNode.js defaultMaximum time to receive complete HTTP headers (milliseconds)
server.keepAliveTimeoutnumberNode.js default valueKeep-alive idle wait time after response completes (milliseconds)
server.socketTimeoutnumberNode.js default valuesocket inactivity timeout (milliseconds), 0 means disabled
server.maxHeaderSizenumberNode.js default valueMaximum request header size (bytes)
server.maxRequestsPerSocketnumberNode.js default valueThe maximum number of requests per socket, 0 means unlimited
server.connectionsCheckingIntervalnumberNode.js default valueOutstanding request timeout check interval (milliseconds)
export default {
  server: {
    requestTimeout: 120_000,
    headersTimeout: 60_000,
    keepAliveTimeout: 5_000,
    socketTimeout: 0,
    maxHeaderSize: 16 * 1024,
    maxRequestsPerSocket: 0,
    connectionsCheckingInterval: 30_000,
  },
};
Tip

config.server only affects inbound service requests; the timeout for outbound app.fetch / app.fetch.proxy is still controlled by config.fetch.timeout or options when calling.

Response configuration (response)

Configuration itemTypeDefault valueDescription
response.wrapbooleantrueWhether to enable export packaging (res.json(data) is automatically wrapped as { code, data, requestId })
response.hideInternalErrorsbooleantrueWhether to hide 500 error details (it is recommended to enable it in production environment and not expose stack trace)
response.logErrors.unknownErrorsbooleantrueWhether to log unknown 500 errors (including complete err object and stack trace)
response.logErrors.http5xxbooleantrueWhether to log HttpError 5xx (error level)
response.logErrors.http4xxbooleanfalseWhether to log HttpError 4xx (warn level, it is recommended to turn it off in high traffic scenarios to reduce log noise)
export default {
  response: {
    wrap: true,
    hideInternalErrors: true,
    logErrors: {
      unknownErrors: true, // Unknown errors must be logged
      http5xx: true, // 5xx is the responsibility of the server
      http4xx: false, // 4xx is not recorded by default (to avoid noise in high traffic scenarios)
    },
  },
};

The response.hideInternalErrors here is aimed at the 500 path of "unknown exceptions", such as throw new Error("...") directly in the code. If you use app.throw(...) to actively throw 404, 409 and other structured HTTP errors, the framework will still return the status code and message you specify, regardless of this configuration.

Actual output of res.json(data) with wrap: true enabled:

{
  "code": 0,
  "data": { "name": "Alice" },
  "requestId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
}

Set wrap: false to turn off wrapping, and res.json(data) will output the original data directly.

Body Parser configuration (bodyParser)

Configuration itemTypeDefault valueDescription
bodyParser.enabledbooleantrueWhether to enable body parsing
bodyParser.maxBodySizestring | number'1mb'Maximum request body size
export default {
  bodyParser: {
    enabled: true,
    maxBodySize: "5mb", // Allow larger request body
  },
};

maxBodySize supports string formats ('1mb'', '500kb'') and numeric formats (number of bytes).

Multipart / File upload configuration (multipart)

Configuration itemTypeDefault valueDescription
multipart.enabledbooleanfalseWhether to enable built-in multipart parsing (req.files will be automatically filled when enabled)
multipart.maxFileSizenumber10485760Maximum size of a single file (bytes, default 10MB)
multipart.maxFilesnumber10Maximum number of files in a single request
multipart.allowedMimeTypesstring[]undefinedWhitelist of allowed MIME types (no restriction if not set)
export default {
  multipart: {
    enabled: true, // Enable built-in parsing
    maxFileSize: 10 * 1024 * 1024, // 10MB
    maxFiles: 5,
    allowedMimeTypes: ["image/jpeg", "image/png", "application/pdf"],
  },
};

Access Log Configuration (accessLog)

Configuration itemTypeDefault valueDescription
accessLog.enabledbooleantrueWhether to enable access log
accessLog.levelstring'info'Basic log level, only supports 'info' or 'debug'
accessLog.skipPathsstring[][]Exact match skipped path list
accessLog.skipPathPrefixesstring[][]List of paths skipped by prefix matching
accessLog.slowThresholdnumber0Slow request threshold, 0 means not enabled
accessLog.warnOn4xxbooleanfalseWhether to promote 4xx responses to warn
accessLog.logResponseSizebooleanfalseWhether to append the response body size
export default {
  accessLog: {
    enabled: true,
    level: "info",
    skipPaths: ["/health", "/ready"],
    skipPathPrefixes: ["/internal"],
    slowThreshold: 1000,
    warnOn4xx: false,
    logResponseSize: false,
  },
};

When enabled, each request is automatically logged on completion:

GET /api/users 200 12ms | 127.0.0.1

OpenAPI configuration (openapi)

Configuration itemTypeDefault valueDescription
openapi.enabledbooleanfalseWhether to enable OpenAPI documentation
openapi.titlestring'API Documentation'Document title
openapi.descriptionstring''Document description
openapi.versionstring'1.0.0'API version number
openapi.docsPathstring'/docs'Scalar documentation path
openapi.jsonPathstring`'/openapi.json''OpenAPI JSON endpoint path (vext internal route registration path)
openapi.jsonPublicPathstringSame as jsonPathThe public path to reference spec in Scalar HTML. The reverse proxy stripping prefix scenario is required. For details, see [Reverse Proxy Deployment](/guide/openapi#Reverse Proxy Path Prefix Scenario)
openapi.scalarobject{}Scalar API Reference UI configuration (theme, dark mode, layout, favicon, etc.)
openapi.serversArray[]List of API servers
openapi.tagsArray[]Tag definition
openapi.securitySchemesobject{}Security schemes
openapi.contactobject{}Contact information
openapi.licenseobject{}License information
export default {
  openapi: {
    enabled: true,
    title: "My App API",
    description: "My App API Documentation",
    version: "1.0.0",
    docsPath: "/docs",
    jsonPath: "/openapi.json",
    scalar: {
      theme: "default",
      darkMode: false,
      layout: "modern",
      favicon: "/favicon.svg",
    },
    servers: [
      { url: "http://localhost:3000", description: "Local development" },
      { url: "https://api.myapp.com", description: "Production environment" },
    ],
    tags: [
      { name: "User", description: "User management related interface" },
      { name: "Order", description: "Order management related interface" },
    ],
    securitySchemes: {
      bearerAuth: {
        type: "http",
        scheme: "bearer",
        bearerFormat: "JWT",
      },
    },
    contact: {
      name: "API Support",
      email: "support@myapp.com",
    },
    license: {
      name: "Apache-2.0",
      url: "https://www.apache.org/licenses/LICENSE-2.0",
    },
  },
};

Request context configuration (requestContext)

Configuration itemTypeDefault valueDescription
requestContext.enabledbooleantrueWhether to enable AsyncLocalStorage request context
export default {
  requestContext: {
    enabled: true,
  },
};
performance tips

Disabling requestContext can improve RPS by about 3-8%, but the following features will be disabled:

  • app.logger automatically carries requestId
  • app.throw() automatically parses the request locale
  • app.fetch automatically propagates requestId

Consider disabling it only in extreme performance scenarios.

Cluster configuration (cluster)

Configuration itemTypeDefault valueDescription
cluster.enabledbooleanfalseWhether to enable Cluster mode
cluster.workersnumber | string'auto'Number of Workers ('auto' = number of CPU cores)
cluster.autoRestartbooleantrueAutomatically restart Worker when it crashes
cluster.maxRestartsnumber5Maximum number of restarts within the time window
cluster.restartWindownumber60000Restart count window (milliseconds)
cluster.restartBaseDelaynumber1000Restart base delay (milliseconds)
cluster.restartMaxDelaynumber30000Maximum restart delay (milliseconds)
cluster.healthCheck.enabledbooleantrueWhether to enable Worker heartbeat detection
cluster.healthCheck.intervalnumber15000Heartbeat detection interval (milliseconds)
cluster.healthCheck.timeoutnumber30000Heartbeat timeout (milliseconds)
cluster.reload.workerDelaynumber2000Time to wait before replacing the next Worker (milliseconds)
cluster.reload.readyTimeoutnumber30000Worker ready timeout (milliseconds)
cluster.reload.shutdownTimeoutnumber10000Worker shutdown timeout (milliseconds)
cluster.pidFilestring'.vext.pid'PID file path
export default {
  cluster: {
    enabled: true,
    workers: "auto", // Automatically detect the number of CPU cores
    autoRestart: true,
    maxRestarts: 5,
    healthCheck: { enabled: true },
    reload: { workerDelay: 2000 },
  },
};

You can also turn on Cluster mode through the environment variable VEXT_CLUSTER=1 without modifying the configuration file.

Dev mode configuration (dev)

Configuration itemTypeDefault valueDescription
dev.errorOverlay.enabledbooleantrueWhether to enable the Dev error overlay (the HTML error page will be displayed when the browser accesses the error route)
dev.errorOverlay.theme'dark' | 'light''dark'Error overlay theme
dev.errorOverlay.maxFramesnumber25The maximum number of stack frames to display
export default {
  dev: {
    errorOverlay: {
      enabled: true, // Set to false to disable HTML error overlay
      theme: "dark",
      maxFrames: 25,
    },
  },
};
only takes effect in development mode

dev configuration items are only read in vext dev development mode, production mode (vext start) automatically ignores all fields.

The Dev error overlay is based on Accept content negotiation, not the HTTP method:

  • Accept: text/html (Browser address bar GET, HTML form POST) → Return to HTML error page
  • Accept: application/json (frontend fetch / axios / curl) -> always returns JSON.

Console logging is not affected by overlay - logging configured with logErrors behaves exactly the same whether the response returns HTML or JSON.

Middleware whitelist (middlewares)

Configuration itemTypeDefault valueDescription
middlewaresArray<string | { name, options }>[]Route-level middleware whitelist
export default {
  middlewares: [
    // Ordinary middleware - string declaration
    "auth",
    "timing",

    // Factory middleware — object declaration (with default parameters)
    { name: "check-role", options: { roles: ["user"] } },
    { name: "cache-control", options: { maxAge: 3600 } },
  ],
};

Only middleware declared in the whitelist can be referenced in the route's options.middlewares.

Access configuration in code

Routing

export default defineRoutes((app) => {
  app.get("/info", async (_req, res) => {
    res.json({
      port: app.config.port,
      runtimeMode: process.env.NODE_ENV,
      configProfile: process.env.VEXT_CONFIG,
      openapi: app.config.openapi.enabled,
    });
  });
});

In service

export default class MyService {
  constructor(private app: VextApp) {}

  getApiBaseUrl() {
    const { host, port } = this.app.config;
    return `http://${host}:${port}`;
  }
}

In plug-in

export default definePlugin({
  name: "my-plugin",
  setup(app) {
    const myConfig = app.config.myPlugin ?? { enabled: false };
    if (!myConfig.enabled) return;
    // ...
  },
});
configuration read-only

app.config is deep-frozen (deepFreeze) after startup and any attempt to modify it will throw a TypeError. This ensures that the configuration is not accidentally modified at runtime.

Custom configuration fields

The VextConfig interface allows extending custom fields. Plug-ins and business code can add arbitrary fields in the configuration:

// src/config/default.ts
export default {
  port: 3000,

  // Custom fields
  redis: {
    url: "redis://localhost:6379",
    db: 0,
  },
  mailer: {
    smtp: "smtp://localhost:1025",
    from: "noreply@myapp.com",
  },
};

Use with declare module to get type hints:

// src/types/config.d.ts
declare module "vextjs" {
  interface VextConfig {
    redis?: {
      url: string;
      db?: number;
    };
    mailer?: {
      smtp: string;
      from: string;
    };
  }
}

Environment variables

In addition to configuration files, some settings can also be controlled through environment variables:

Environment variablesDescription
VEXT_CONFIGSelect the config profile to load
NODE_ENVRuntime mode; vext start runs as production
PORTCan be referenced in default.ts as process.env.PORT
VEXT_PORTInternal pass variable of CLI --port, higher priority than provider patch
VEXT_HOSTCLI --host internal pass variable, higher priority than provider patch
VEXT_PORT_CONFLICTPort conflict policy: error / prompt / kill / next
VEXT_LIFECYCLE_LEVELLifecycle log level: concise / verbose
VEXT_CLUSTEREnables Cluster mode when set to 1
// src/config/default.ts — use environment variables
export default {
  port: Number(process.env.PORT) || 3000,
  logger: {
    level: process.env.LOG_LEVEL || "info",
  },
};

:::warning Security Tips Do not hardcode sensitive information (such as database passwords, API Keys) in configuration files. Recommended:

  • Use environment variable: process.env.DB_PASSWORD
  • Use local.ts (added .gitignore) to store sensitive configurations for local development :::

Configuration verification

config-loader will perform Fail Fast verification after the merge is completed, checking the following:- port must be a positive integer in the range 1-65535

  • adapter must be a known built-in identifier or a valid adapter object/function
  • Each element in the middlewares array must be a string or a { name: string } object
  • rateLimit.max must be a positive integer
  • rateLimit.window must be a positive integer
  • logger.level must be a legal log level
  • logger.redactKeys / logger.redactPaths must be a string array, logger.redactValue must be a string
  • shutdown.timeout must be a non-negative number (unit: seconds)
  • server.requestTimeout, server.headersTimeout, server.keepAliveTimeout, server.socketTimeout must be non-negative finite numbers (unit: milliseconds)
  • server.maxHeaderSize, server.connectionsCheckingInterval must be positive integers, server.maxRequestsPerSocket must be non-negative integers
  • cluster.workers must be a positive integer or 'auto' / 'auto-1'

If the verification fails, the framework will report an error immediately at startup and give a clear error message to avoid configuration errors being exposed at runtime.

Complete example

// src/config/default.ts
export default {
  port: Number(process.env.PORT) || 3000,
  host: "0.0.0.0",
  adapter: "native",
  trustProxy: false,

  logger: {
    level: "info",
  },

  cors: {
    origins: ["*"],
    credentials: false,
  },

  rateLimit: {
    enabled: true,
    max: 100,
    window: 60, // unit: seconds
    keyBy: "ip",
  },

  requestId: {
    enabled: true,
    header: "x-request-id",
  },

  bodyParser: {
    enabled: true,
    maxBodySize: "1mb",
  },

  accessLog: {
    enabled: true,
    level: "info",
  },

  response: {
    wrap: true,
    hideInternalErrors: true,
  },

  shutdown: {
    timeout: 10, // unit: seconds
  },

  server: {
    requestTimeout: 120_000, // Maximum time to receive a complete request, unit: milliseconds
    headersTimeout: 60_000, // Maximum time to receive complete request headers, unit: milliseconds
    keepAliveTimeout: 5_000, // keep-alive idle waiting time after the response is completed, unit: milliseconds
    socketTimeout: 0, // socket inactivity timeout, 0 means disabled
    maxHeaderSize: 16 * 1024, // Maximum request header size, unit: bytes
    maxRequestsPerSocket: 0, //The upper limit of the number of single connection requests, 0 means no limit
    connectionsCheckingInterval: 30_000, // Timeout check interval for unfinished requests, unit: milliseconds
  },

  requestContext: {
    enabled: true,
  },

  openapi: {
    enabled: true,
    title: "My App API",
    version: "1.0.0",
  },

  frontend: {
    enabled: true,
    framework: "react",
    publicDir: "public",
    publicPath: "/",
  },

  middlewares: ["auth", { name: "check-role", options: { roles: ["user"] } }],

  // Custom configuration
  redis: {
    url: process.env.REDIS_URL || "redis://localhost:6379",
  },
};
// src/config/production.ts
export default {
  logger: { level: "warn" },
  cors: { origins: ["https://myapp.com"], credentials: true },
  openapi: { enabled: false },
  // logger.level: "warn" will suppress normal info/debug access logs; 5xx will still be promoted to error.
  accessLog: { level: "info", warnOn4xx: true },
  cluster: {
    enabled: true,
    workers: "auto",
  },
};
// src/config/local.ts — do not commit to Git
export default {
  port: 8080,
  redis: {
    url: "redis://localhost:6380",
  },
};

Next step