Routing
VextJS combines convention-based file routing with route declarations inside defineRoutes(). A route file maps to a URL prefix; its factory declares the methods and paths beneath that prefix.
Start with a runnable route, then learn file mapping, request validation, and business integration. For the full fields and defaults, see the Route Definition API; for binding rules, see the HTTP and Routing Specification.
The route-demo.ts below is a complete, standalone file for an existing VextJS project. Other snippets that mention app, req, res, or handler belong inside their respective factory or handler. The business example declares its service and authentication prerequisites separately.
Run a route first
1. Create the route file
Prerequisite: a VextJS project created according to Quick Start, with dependencies installed and npm run dev working. Create this file; it needs no database, custom service, or authentication middleware.
2. Verify paths and validation
Run npm run dev from the project root. In another terminal, use the actual port shown on startup. Save these two request files in the project root so that JSON quoting works consistently across shells:
route-valid.json:
route-invalid.json:
Run these commands from the same directory. In Windows PowerShell, use curl.exe in place of curl:
The file prefix is /route-demo, so write "/" or "/:id" inside the file; do not add /route-demo again. The response wrapper depends on app configuration, and exact validation messages can vary with the validator and locale.
3. Integrate business logic
After the minimal route works, pass business operations to a service. Add validation, middleware, authentication, and response declarations as needed.
The local app.get(...) snippets below belong inside defineRoutes((app) => { ... }). Names such as handler, user, data, and app.services.* stand for application code; VextJS does not create those business capabilities automatically.
The factory must be synchronous; handlers may be async. Register routes with direct top-level statements in the factory body, not inside loops, conditionals, or async callbacks. Statically resolvable function bindings and default re-exports are supported; see the factory rules.
Basic concepts
File routing mapping
Route files under src/routes/ that pass the loader rules map to URL prefixes. The table shows alternative layouts: users.ts and users/index.ts cannot coexist.
Three-stage definition
VextJS routing is defined using three-part (path, options, handler) or two-part (path, handler):
The second parameter, options, is a declarative configuration object. Common fields follow; for response, cache, upload, and other fields, see RouteOptions.
How to write routing files
Each route file default-exports the result of defineRoutes(). Verify the path and validation with the standalone example above before moving business operations into a service. Do not put the database connection, authentication implementation, and an entire CRUD application into your first route.
Use the two-part form for a simple endpoint such as a health check. Use the three-part form for validation, middleware, access protection, or response declarations. A factory can declare multiple methods, but each registration must be a direct statement in its body. For loading rules, see “Route loading priority” and “Exclusion rules” below. For the exact signature, see the defineRoutes API.
To create, read, update, and delete a resource, see Business route composition. That section names the service and authentication prerequisites explicitly.
Route loading priority
When routes might conflict, router-loader applies these rules:
- Static paths take precedence over dynamic paths:
/users/listbefore/users/:id. - Files sort alphabetically for deterministic loading.
- Both file prefixes and final route identities are checked: the static index rejects
routes/users.tsalongsideroutes/users/index.ts. Runtime checks also reject duplicate normalized HTTP method and full path pairs, including case and trailing-slash variants. Different final paths do not bypass the file-prefix restriction. - HEAD precedes GET at the same path, and specific paths precede wildcard paths. Do not rely on filename order to override an existing route.
Exclusion rules
Supported route sources are .ts, .js, and .mjs. A .cjs route source fails loading; it is neither supported nor silently excluded. The loader skips:
- Test files:
*.test.tsand*.spec.ts - Type declarations:
*.d.ts - Files or directories starting with
_or. node_modulesdirectories- Generated temporary files containing
.__vext_compiled__
These skipped files are not startup errors. Runtime loading, route diagnostics, and manifest generation use the same exclusion policy. The _ prefix can hold shared route utilities:
HTTP method
The app object in the defineRoutes() callback supports these HTTP methods:
Dynamic routing parameters
File-level dynamic parameters
Use [paramName] as the file name or directory name to automatically convert it to a routing dynamic parameter:
Dynamic parameters within the route
The :paramName syntax can also be used in routing paths inside files:
Request object (req)
Handlers read HTTP input through req. For business input, prefer req.valid() for locations declared in the validation schema: it contains validated, converted values. Raw req.params/query/body/headers/cookies remain available.
Only declared locations produce validation results; an undeclared location returns undefined. Field optionality comes from the schema; a TypeScript generic cannot substitute for runtime validation. The id example above converts a string into a number. A handler can apply business defaults to optional fields:
For method, URL, raw input, request ID, IP, protocol, cookies, session, and app instance, see request members. See req.valid() for signatures and inferred types. Enable Session before accessing it. See Uploads for file reads and regular field limits.
Use req.onClose() to clean up timers and other resources for long connections or streams. It runs on normal response completion or early connection closure, at most once per callback. Registering after completion runs the callback immediately. A callback does not imply an abnormal disconnect, and normal completion does not abort req.signal. Check signal separately when cancelling downstream work.
Response object (res)
Call res.json(data) in a handler to send ordinary JSON business data. Pass 201 for creation and use 204 for a successful deletion with no body. Merely returning data does not send a response.
Each line is an alternative for a different request; do not send them in sequence for one request. By default, config.response.wrap: true wraps JSON as { code: 0, data, requestId }; a 204 response has no body. See JSON response for fields, defaults, and behavior with wrapping disabled.
Choose other response methods according to the task; the request and response API has exact parameters and examples:
To constrain JSON output fields, see top-level responses under “OpenAPI documentation configuration.” Use app.throw() for errors; do not pass error responses as successful data to res.json().
Parameter validation
VextJS integrates schema-dsl, declares validation rules in the route options.validate, and the framework automatically performs validation and generates OpenAPI documents.
DSL syntax at a glance
The introduction uses integer:1-! and string:1-50!: ! makes a field required, and the range constrains its value or length. Use ? (or omit the required marker) for an optional field. Declare the rule in route options and read the converted result in the handler.
For strings, numbers, email, URL, booleans, dates, and enums, see the DSL syntax guide and route validation reference. A schema does not decide whether an email is already registered or whether a user owns a resource; implement those business and authorization checks separately.
Validation locations
Validation runs in this order: param → query → header → cookie → body. An invalid path param returns HTTP 400 immediately; failure at another location returns HTTP 422 immediately.
Validation error response
When validation fails, the framework returns a structured error response:
Routing level middleware
Use options.middlewares to attach middleware to a route. This is a composition snippet: first create the audit-log and response-label files described in Define middleware, then allowlist them in configuration. handler represents your own business handler.
Custom route middleware runs in declaration order, before automatic route validation. Before next(), do not assume req.valid() has validation results. An authentication middleware must establish req.auth before a route's auth guard can enforce protection.
The factory argument here comes from the response-label definition. Route options replace that middleware's configured default options as a whole. Configure built-in rate limiting with global rateLimit.enabled and route override.rateLimit; window is measured in seconds. See override.
OpenAPI document configuration
OpenAPI documentation information for configuring routing via options.docs:
Top-level responses is the runtime contract used for compiled JSON
serialization, OpenAPI, and generated client types. Keep descriptions and
examples in docs.responses; do not repeat the schema there for the same
status selector.
Hidden route
To omit a route from generated OpenAPI documentation, set docs.hidden: true. This does not block HTTP access; use authentication and authorization for access control:
Access the app object
The app argument to defineRoutes() gives access to services, logging, errors, and configuration:
A route handler can access app in either way:
- Closure
app: the argument todefineRoutes((app) => ...). req.app: the real runtime application reference on the request.
The factory's app is a Proxy facade backed by the real application. Reads of app.config, app.services, and extension properties forward to the real application; these are not property snapshots copied into a collector. req.app points to the real application.
Use req.app.fetch in a handler for attached methods such as fetch.get() and fetch.create(); see the HTTP client guide.
If you assign const config = app.remoteConfig outside request handling, that variable retains the value read at that moment. For the latest value, read app.remoteConfig or req.app.remoteConfig inside the handler. This is ordinary JavaScript reference capture, regardless of which app entry point you choose.
The factory's HTTP registration methods close after collection. Calling app.get() from a handler fails.
Error handling
app.throw() — throw HTTP error
When using app.throw() in a route or service to throw an error, the framework will handle it uniformly and return a structured response:
app.throw() will terminate the current request processing flow (function signature returns never), no need to add return after it.
If an unexpected exception is thrown here, you can also directly:
The framework will catch it as well, but this path represents an "unknown runtime error" and will ultimately return a 500 Internal Server Error. In the development environment, when response.hideInternalErrors = false, the JSON 500 response will be accompanied by stack; if your goal is to actively return a clear 4xx/5xx HTTP result, you should still use app.throw(...) first.
Business route composition
This article-creation example connects HTTP input to business operations and an HTTP response. Before running it, implement a post service and provide an allowlisted auth middleware that sets req.auth.userId. It is a business wiring snippet; without those prerequisites, start with route-demo.ts above.
For a full CRUD resource, keep the same responsibilities:
A status enum can use draft|published|archived. Validation constrains declared input. auth.required does not automatically check ownership, status, or database uniqueness. See Services and Security for implementation, and CRUD API for an example with its application dependencies.
Next step
- Understand how the service layer organizes business logic
- Learn the onion model of middleware
- Explore the advanced usage of Parameter Validation
- View OpenAPI Documentation automatically generated
- Check stable Rule IDs in the HTTP and Routing Specification