Route definition
This page details the route definition API of VextJS, including defineRoutes, routing options, parameter validation, middleware references and document configuration.
defineRoutes
defineRoutes is the core function for creating route files. It receives a factory callback in which the route is registered via the app object.
Function signature
The route factory must be synchronous: do not mark it async and do not
return a Promise. Individual route handlers may still be async. This keeps
runtime registration, build indexing, Doctor, and type generation on the same
statically projectable route set.
Working principle
- When
defineRoutes(factory)is called, a collector (route collector) is created internally factory(collector)is executed, andapp.get/post/...in the user code actually calls the collector method.- Each route is pushed into the internal
routesarray - Return the
RouteDefinitionobject router-loaderscans thesrc/routes/directory and callsregister()on thedefault exportof each file to register with the underlying adapter
In the factory callback, app not only has HTTP methods (get/post/put/...), but also can access complete capabilities such as app.services, app.config, app.throw, app.logger, etc. These properties are injected by router-loader before executing the factory.
Route registration syntax
VextJS supports two route registration syntaxes: three-stage and two-stage.
Three-stage (recommended)
Complete syntax with options configuration, supporting parameter verification, middleware reference, document configuration, etc.:
Two-stage
Simplified syntax without options, suitable for simple routes that do not require validation, middleware or document configuration:
Supported HTTP methods
Routing path
Static path
Dynamic parameters
Use :paramName to define dynamic path parameters, accessed through req.params or req.valid('param'):
If a dynamic path reads from req.params without declaring validate.param, OpenAPI automatically adds a required: true string path parameter for :paramName or *paramName so the generated path template is valid. Declare validate.param when you need format constraints.
Wildcard
File routing mapping
The directory path of the routing file is automatically mapped to the URL prefix:
The path registered in the routing file is a relative subpath, and the framework automatically splices the file path prefix. For example, app.get('/:id') in src/routes/users.ts is ultimately registered as GET /users/:id.
RouteOptions
The second parameter of the routing three-part syntax is the declarative configuration object.
Frontend freshness
RouteOptions.frontend keeps page freshness on the existing route declaration:
staticParams is valid only for "static". revalidate is valid only for
"revalidate" and is a positive interval in seconds. clientOnly keeps the
route document/data/assets while intentionally skipping the server page body;
it is not PPR or a second page route.
hydration: "none" does the opposite of clientOnly: it requires and keeps
the SSR page body but removes the Vext/React browser runtime, hydration data,
and route JS preload. It cannot be combined with clientOnly or disabled SSR.
seo is static, JSON-safe route metadata and is merged before per-render SEO.
Build-indexed paths and route metadata use a finite static grammar so the build
index and runtime cannot diverge. The index accepts literals, same-file const
bindings, and TypeScript as const / simple as Type / satisfies wrappers.
A route-options helper call is rejected because the index does not execute the
helper body and cannot know whether it adds, removes, or replaces contract
fields. Inline the helper's final object or store that final object in a
same-file const. Comments, strings, template text, and regular expressions
are ignored during structural matching.
Each app.get(...) / app.post(...) registration must be a direct top-level
statement in the defineRoutes callback. Conditional or nested registration
fails the static projection because the build index cannot guarantee whether
runtime control flow executes it.
Imported values, computed expressions, and template literals with
interpolation are not executed. If a route path, validate location, or
response schema cannot be projected, build/doctor/typegen fails with file,
HTTP method, and route context instead of silently omitting the route or
emitting an empty contract. Use res.render(..., { seo }) for
request-dependent metadata. See
SEO, Sitemap, and Robots.
Complete example
validate
Declarative parameter validation, based on schema-dsl DSL syntax. The
framework validates before the handler runs. An invalid param (path
parameter) returns HTTP 400; invalid query, header, cookie, or body
data returns HTTP 422.
The field type is VextSchemaField, which supports schema-dsl strings, field-level DslBuilders, nested objects, and object arrays. Field-level DslBuilder is often used to add business descriptions to OpenAPI documents:
The static projector recognizes only schemaAdapter imported by name from
vextjs (an alias is allowed), compileField(<static string>), and at most one
.description(<static string>). The complete builder may be stored in an
unambiguous same-file const. Imported builders, dynamic arguments, other call
chains, and opaque Zod/Yup objects fail the build instead of producing a partial
request contract.
These descriptions will enter the OpenAPI schema while retaining constraints such as required, enumeration, and length.
Verify location
Verify execution order: param → query → header → cookie → body
Basic usage
DSL syntax quick check
schema-dsl will automatically do type conversion. For example, '2' (string) in the query parameter ?page=2 will be automatically converted to 2 (number), provided that the schema is declared as 'number' type.
Get the verified data
Use req.valid(location) to obtain the verified and type-converted data:
The handler type is inferred from the route schema without a duplicate interface:
An explicit generic remains available only as an escape hatch for dynamic or external schemas and overrides the inferred contract.
Verification failure response
For query, header, cookie, or body, validation failure returns HTTP
422 with a structured response such as the following. A validate.param
failure uses the same error shape with HTTP/code 400 because the URL path is
invalid.
middlewares
Route-level middleware reference. The referenced middleware must first be declared in the config.middlewares whitelist.
String reference
Object reference (with configuration override)
VextMiddlewareRef type
Execution order
Routing-level middleware is executed after global middleware and before handler:
Configure whitelist
Middleware referenced in routes must be declared in the configuration file:
References to middleware not declared in the whitelist will throw an error on startup:
auth
RouteOptions.auth is the route guard contract. It is separate from identity parsing:
auth()middleware reads the request credential and fillsreq.auth.auth: truerequires an authenticated request.- Object form can require roles, scopes, permissions, or a custom
check. auth: { required: false }makes identity optional; without roles, scopes, permissions, orcheck, OpenAPI marks the route as public.auth: falsemarks the route as explicitly public and disables legacy OpenAPI security inference frommiddlewares.
The build index accepts the final inline object or a same-file const such as updatePostOptions. It rejects route-options helper calls because it does not execute helper bodies. Keep each route's complete guard contract in one of these statically projectable forms; shared runtime authorization logic still belongs in middleware or the permission provider.
Runtime auth, OpenAPI security, and Docs access
These are related but independent layers:
auth.roles,auth.scopes,auth.permissions, andauth.checkare runtime route guards. They decide whether the current request reaches the handler.auth.securityis OpenAPI metadata. It selects the documented security scheme, and an object array can declare OAuth scopes such as[{ oauth2: ["posts:write"] }]; it does not grant or enforce that scope.docs.securityonly overrides the generated OpenAPI security metadata. It does not disable a runtimeauthrequirement.docs.accessis Vext Docs visibility/Try it out metadata sent toopenapi.docs.access.resolver. It does not protect the route; useauthfor API access control.
It is valid for an application to use the same string in a runtime scope and an OAuth scope, but they remain separate declarations. Keep both explicit when both are required.
Guard failures use stable error codes:
requestContext.getStore()?.auth stores only a safe snapshot of identity metadata. It intentionally excludes raw credentials and claims; use req.auth inside the route when provider claims are needed.
cache
Route-level response cache configuration. Response caching occurs on the server side and caches interface response content; it is not custom middleware, nor is it the browser Cache-Control response header.
Commonly used writing methods:
See the Response Caching Guide for details.
docs
OpenAPI documentation configuration, controls how routes are displayed in automatically generated API documentation.
RouteDocsConfig
Field description
docs.access is emitted on the OpenAPI operation as the x-vext-docs-access vendor extension and passed to openapi.docs.access.resolver as the access field of a kind: "operation" descriptor during Vext Docs filtering. String values are useful for role, tenant, or group labels; object values can carry roles, permissions, group, visible, and tryItOut metadata. This is documentation access metadata only: hiding an operation or disabling Try it out does not add authentication or authorization to the route.
Complete example
operationId automatically inferred
When operationId is not specified, the framework is automatically generated based on the HTTP method and path:
Explicit docs.operationId values and inferred operationId values share the same global uniqueness constraint. If a conflict exists, OpenAPI generation fails; set a unique docs.operationId on the conflicting route or change the route method/path so inferred values differ.
Hidden route
Mark obsolete
Security solution coverage
By default, security schemes are inferred in this order:
docs.securityif explicitly set, including[].RouteOptions.authwhen it istrueor an object;auth: { required: false }without roles/scopes/permissions/check emits public security.- Legacy
middlewaresinference throughconfig.openapi.guardSecurityMap.
auth:false disables the legacy fallback for that route. If auth: { required: false } also declares roles, scopes, permissions, or check, runtime still requires authentication and OpenAPI emits authentication security.
Can be manually overridden:
Runtime response schema
Declare this map as top-level RouteOptions.responses. Selectors support an
exact status (201), a family (2xx), or default; the final status after
response:before chooses exact → family → default. Vext compiles each JSON
schema once during route registration and reuses it across requests. The same
closed schema is projected to OpenAPI, route manifests, static build indexing,
and generated client types.
Schemas describe the business data passed to res.json(), not a manually
duplicated envelope. Undeclared properties are removed recursively. Missing
required values fail before bytes are committed. HEAD, exact 204, raw JSON,
text, redirect, file/download, stream, and render/SSR responses bypass this
serializer. See OpenAPI response contracts
for lifecycle and raw JSON Schema details.
Documented response metadata
Keep descriptions, examples, headers, and content type in docs.responses.
Do not repeat schema there when the same normalized selector already exists
in top-level responses; registration fails on this dual declaration.
Multi-example response:
Custom response header:
multipart
Route-level file upload configuration. multipart.files automatically outputs an OpenAPI multipart/form-data requestBody without manually writing docs.requestBody. Set multipart.enabled: true to opt one route into built-in parsing when global config.multipart.enabled is off; set multipart.enabled: false to opt one route out when global parsing is on. Built-in parsing is memory-only: it creates no framework-managed temporary files, so there is no tmp directory, file TTL, or periodic cleanup setting. Use a streaming upload plugin for large files or persistent storage.
When a required file field is missing, Vext returns 400 with the missing field names. Optional fields and undeclared upload fields are accepted; they are still limited by maxFiles, maxFileSize, and allowedMimeTypes.
multipart.files and validate.body are mutually exclusive. When configured at the same time, multipart.files takes priority in OpenAPI document generation.
session
Controls Session for one route. false opts out of a globally enabled Session runtime. true opts in when the global runtime is disabled. The object form also overrides rolling and autoCommit; Store identity, cookie name, and session id length remain application-level settings.
override
Route-level configuration override, overrides the global configuration in src/config/default.ts.
Routes can set top-level { timeout: number } to enforce a positive request deadline in milliseconds and send HTTP 504 on timeout. Top-level { timeout: false } explicitly disables the route timeout middleware and takes precedence over the legacy override.timeout field.
Routes can also set top-level { securityHeaders: false } when an embeddable page, webhook callback, or fully custom response header stack must skip the global Security Headers preset.
RouteDefinition
The route definition object returned by defineRoutes() (internal data structure, usually does not need to be manipulated directly).
Factory and collector internals are not part of the public object shape and should only be driven through defineRoutes() and the router loader lifecycle.
RouteRecord
Internal data structure of a single route:
VextHandler
Type definition of route processing function:
Handler is the last link in the middleware chain and does not call next().
Basic example
Access App Capabilities
In the factory callback of defineRoutes, access app through the closure:
If you want to actively return clear HTTP errors such as 404, 401, 409, etc., you should use app.throw(...) first. The normal throw new Error("...") will also be caught by the framework, but it represents an unknown runtime exception and will eventually go down the 500 error path; field-level validation failures should use VextValidationError.
Multiple route registration
Multiple routes can be registered in a routing file:
Notes
Do not call HTTP methods directly on the app
The app returned by defineRoutes is a collector, not a real application instance. Calling the HTTP method directly on the application instance throws an error:
The routing file must be default export
Build-time consumers accept a finite default-export grammar. defineRoutes
must be a named import from vextjs (an import alias is allowed), and the
factory must be an inline synchronous arrow or function expression:
Re-exports, imported route definitions, property/namespace callees, callback identifiers, and files without a supported default export fail with the route file in the diagnostic.
Routing path normalization
The framework automatically handles the following path edge cases: