Parameter validation
VextJS integrates schema-dsl for declarative parameter validation. In route options.validate, use DSL strings or supported field schemas. The framework validates and converts inputs; when OpenAPI is enabled, it projects supported rules into documentation. Parameter validation does not provide authentication, authorization, or database uniqueness.
Basic usage
Prerequisite: a TypeScript project with dev, build, and start scripts created from Quick Start. These configuration and route files form a standalone API example; merge them into an existing project as appropriate. It echoes validation results to show conversion and errors without a business service.
Declare rules in the validate field of a three-part route:
Run npm run dev. When the ready message shows the listening address, send requests from another terminal:
Expected results, in order: 201 (data.age is number 42), 422 (invalid email), 200 (data.id is number 42 and active is true), 400 (path param fails first), and 422 (string 1 is not accepted as boolean). Valid JSON syntax does not imply valid fields. Malformed JSON is usually rejected earlier by the body parser with 400.
In Windows PowerShell, use curl.exe for GET. For JSON POST, avoid shell quoting differences with:
Stop the dev server with Ctrl+C. Run npm run build and npm start, then repeat the requests to confirm the production behavior. If the port is busy, change the example configuration and request URL or stop your own old example process.
Later snippets without a file header are independent route fragments. Their handler, business services, and auth middleware must be supplied by your app; do not register every fragment unchanged in one app.
After validation passes, read converted data through req.valid(location). Invalid path param returns HTTP 400; invalid query, header, cookie, or body returns HTTP 422 before the handler runs.
Validation locations
validate supports five locations, corresponding to different data sources requested:
Validation runs in the order param → query → header → cookie → body and stops at the first failed location. Rules are compiled at route registration and reused for requests. Use lowercase header field names; req.valid("header") projects only declared fields and returns lowercase keys. A parsed cookie string is not proof of a valid cookie signature or login session.
::::tip note
The singular param is used in validate (corresponding to the concept of path parameters), but the underlying data source is req.params (plural). The mapping has been done correctly inside the framework, you don’t need to worry about it.
If a dynamic path uses :id or *path without validate.param, OpenAPI still emits a required: true string path parameter for that segment so the path template remains valid. Declare validate.param when you need stricter type, length, or format constraints.
::::
Detailed explanation of DSL syntax
schema-dsl uses concise string expressions to describe data types and constraints.
Basic types
Required and optional
Add a ! or ? tag at the end of the type expression:
! requires the field to exist; it does not require a nonempty value. "string!" accepts an empty string. Use "string:1-!" when empty strings must fail. ? allows omission but does not allow null; see field schemas below for explicit nullability. Optional fields do not receive business defaults automatically. Set pagination defaults in the handler or use a supported schema default.
Range constraints
Use the :min-max syntax to specify a range:
String length
Number range
Enumeration value
Use | to separate enumeration options:
The bare | shorthand describes a string enum and projects as an OpenAPI enum. For numeric enums, use an explicit definition such as enum:number:1|2|3; do not conflate numeric and string enums.
Combination example
Field-level JSON Schema and documentation consistency
A request location is a field map. Each field may use JSON Schema. This complete route demonstrates required arrays, explicit nullability, and defaults:
POST {"label":"ok","tags":["guide"],"note":null} to /shapes: expect 200 and data.limit equal to 20. A missing or single-character label, or an empty tags array, returns 422. Here "label!" and "tags!" mark required fields in the field name; required: ["field"] applies to a JSON Schema object node and lists required child fields. For a simple string array, array<string> DSL also works. Do not use ["string"] as an array shorthand.
The runtime compiler interprets bare field-level type and enum, and OpenAPI and typed clients project those field facts. A root field map may contain a business field named type. For a nested object with a type field, write { metadata: { type: { type: "string" }, label: "string!" } } explicitly; metadata.type: "string" otherwise identifies the whole metadata as a raw schema type.
? allows omission only. To allow null, use types:string|null or { type: ["string", "null"] }. Runtime validation, TypeScript inference, and static projection each need checking; success in one does not establish support in the others.
Type conversion
The default engine converts supported types, especially URL parameters that arrive as strings. This table describes the current dependency; a custom validator may behave differently:
Read validated data
req.valid(location)
Use req.valid() for validated, converted data. Only a declared location produces a result; an undeclared one returns undefined, as described below.
Boundary behavior
::::warning Notes
req.valid(location) has the following boundary behavior to be aware of:
-
Called when
validateis not configuredIf the route is not configured with a
validatefield, callingreq.valid("body")will returnundefined. The framework won't throw an error, but you won't be able to get the data after the checksum typecast. -
location is not declared in
validateIf only
bodyis declared invalidate, butreq.valid("query")is called,undefinedwill also be returned. Only locations explicitly declared invalidatewill have validated data. -
The handler is not reached when validation fails
Before the handler runs, an invalid path
paramreturns HTTP400, while an invalidquery,header,cookie, orbodyreturns HTTP422. Data read throughreq.valid()inside the handler has therefore passed validation. -
Passing validation does not remove every undeclared field
The default engine retains unknown fields. Validating
{ name: "string!" }against{ name: "Alice", extra: 42 }still leavesextrain the result; headers have the separate projection behavior above. Explicitly select fields for business writes. If replacing the validator, check whether it retains, strips, or rejects unknown fields.
Best Practice: Always ensure that the location of req.valid(location) is consistent with the location declared in validate.
::::
Automatic route-schema inference
The handler is contextually typed from the same validate object used at
runtime. You do not need to duplicate that contract as a TypeScript interface:
Inference covers DSL strings, required/optional markers, nested objects, and recognizable field-level JSON Schema. Preserve literal types (for example with as const) when extracting a schema into a variable; widening to plain string loses field-specific inference. Static types alone do not prove runtime compilation or static projection supports the shape. Do not use ["string"] or [{ code: "string!" }] as array shorthand; use explicit { type: "array", items: ... } or supported array DSL. A chainable schemaAdapter.compileField() builder intentionally infers as unknown, because later dynamic mutations are not visible in its static type. The explicit form
req.valid<ExternalBody>("body") remains available as an escape hatch for
dynamic or externally supplied schemas; it overrides inference and therefore
must match the runtime contract maintained by the application.
Validation error response
Validation failures use one structured error shape. Invalid path param data returns HTTP/code 400; invalid query, header, cookie, or body data returns HTTP/code 422. The following is a 422 example:
code: 422 in this example; 400 for a path param errormessage:"Validation failed"for default route validationerrors: field-level error array, includingfield(field name) andmessage(error description)requestId: the unique identifier of the current request
Validation errors are handled uniformly by the framework's global error handler, and you do not need to manually try-catch in routing.
The field messages above illustrate the shape. Actual wording depends on the validator and locale, and a custom error handler can change the response. Do not treat these English sentences as stable business error codes.
Linkage with OpenAPI documentation
OpenAPI is disabled by default. To project this page's example, add openapi: { enabled: true } to the earlier configuration and restart. Supported validate rules project to parameters and requestBody. Business meaning, permission, and response contracts still need separate declarations:
With OpenAPI enabled, this route projects:
page: query number, minimum 1limit: query number, minimum 1, maximum 100status: query string enum ofactive,inactive, andbanned
The default endpoints, when enabled, are /docs and /openapi.json; see the OpenAPI guide. If pagination requires integers, change number to integer and verify that fractional input fails.
For business descriptions, use the explicit builder without global side effects. Vext does not install a global String .description() method:
This route echoes validation results; it does not call a translation service. POST JSON with content and targetLanguages to /translate to inspect descriptions and nested code constraints. Generated OpenAPI retains descriptions, required, enum, minLength, and maxLength. String DSL without a handwritten description receives a fallback description; raw JSON Schema fields need explicit business descriptions where appropriate.
Build-time projection recognizes schemaAdapter as a named import from vextjs, including aliases. The finite builder grammar is compileField(<static string>) with at most one .description(<static string>). A complete builder may be stored in an unambiguous same-file const or resolved through analyzable source bindings. Dynamic arguments, other chains, unresolvable imports, and opaque Zod/Yup values fail with route context; not every import is unsupported.
? means optional, not nullable. Use types:string|null or raw { type: ["string", "null"] } to allow null explicitly.
Advanced usage
Multi-position combination verification
The same route can verify multiple locations at the same time:
Cooperate with routing-level middleware
The verification middleware is executed after the routing-level middleware and before the handler. This means:
If an authentication middleware or guard rejects a request, it does not reach later schema validation. Merely extracting identity without requiring authentication does not reject anonymous requests. A cache hit or earlier middleware short-circuit also skips schema validation and the handler. The next example assumes auth and check-role are fully implemented and registered:
Route override current limiting rules
Beyond validation, options.override can adjust rate limiting, timeouts, and other route behavior. First enable the limiter globally: a route override does not install it, and the default IP key does not automatically isolate budgets by path. See Rate Limiting for full verification.
Reuse the verification engine in the service layer
For route entry parameters, RouteOptions.validate + req.valid() is preferred. If the service also needs to verify non-HTTP input, such as scheduled tasks, message queues, external callbacks, or internal DTOs, you can obtain the current global validation engine through this.app.getValidator().
getValidator() returns the current synchronous VextValidator, backed by schema-dsl by default. Replace it before route registration and service schema compilation; already saved compiled functions do not update when setValidator() is called later. Throw VextValidationError to preserve field errors: through the HTTP error handler, it returns 422 with errors; direct Job or other callers receive an exception to handle themselves. An ordinary Error in the HTTP chain follows the unknown-error 500 path.
::::tip
Use app.getValidator() when validation behavior should be shared. A direct independent schema library bypasses framework replacement. If you need a separate engine, document its syntax, conversion, and error contract differences.
::::
Replace verification engine
VextJS uses schema-dsl as the validation engine by default. If you prefer third-party verification libraries such as Zod and Yup, you can replace the built-in verification engine through plug-ins.
Using Zod Example
This application adapter requires npm install zod and uses the public Zod 4 types. It translates only the listed string subset and falls back to the original engine for an entire schema if it finds any unsupported field. z.looseObject() preserves undeclared fields; see the official Zod object docs. Formatting, conversion, and error text can still differ between engines. The synchronous VextValidator cannot accept refinements or transforms that require async parsing.
app.setValidator() replaces runtime compilation. It does not widen the public type of RouteOptions.validate or change the static route-source grammar. Route declarations must keep using serializable Vext schema values (DSL strings, nested literals, or the canonical schemaAdapter builder); the adapter translates that contract to Zod or Yup internally. Do not place opaque third-party schema instances, including opaque Zod/Yup objects, in RouteOptions.validate. Build, Doctor, OpenAPI, and client contracts must project the route before plugin setup runs.
After installing and enabling the plugin, add a route that only uses its supported subset:
POST {"name":"","email":"alice@example.com","extra":42} to /zod-check: expect 200 with the empty name and extra retained. Missing name or an invalid email returns 422. Number and boolean rules in the basic example are outside this adapter's subset and should fall back to the original engine with its conversions. Then build and start the production app and repeat the HTTP checks. Compare only the promised subset; this example does not establish full equivalence between validators.
Common patterns
Pagination query
Search filter
User registration
File path parameters
This is a fragment inside the defineRoutes callback of src/routes/files/[id].ts. It requires an application file service that supplies stream, name, contentType, and metadata. The file prefix provides /files/:id:
Best Practices
1. Always use req.valid() instead of req.body
Routes configured with validate should use req.valid('body') instead of directly accessing req.body:
req.valid() reads the validator's stored result.data. The framework does not assign it back to req.body or req.query. Whether an individual validator mutates input in place is engine behavior; do not assume the raw and validated objects are always identical or always different.
2. Reasonable use of required tags
Requiredness comes from the API contract, not the input location. Pagination query may be optional if the handler supplies defaults; core create fields are usually required, and a required header should be marked required too:
3. Verification rules are documents
Validation rules give the structural part of an input contract. A complete document also needs purpose, identity requirements, responses, errors, and business constraints. State field constraints precisely:
4. Use app.throw() for custom validation in Handler
DSL syntax cannot cover all verification scenarios (such as cross-field verification, database uniqueness checking). For these scenarios, use app.throw() in the handler or service to throw manually:
An application-level check before a write cannot guarantee uniqueness under concurrency. Handle a database uniqueness conflict too. Schema validation does not replace authorization, resource ownership, inventory, or payment eligibility; see the Validation and Contracts Specification.
Troubleshooting and verification
Next step
- Understand the global configuration related to verification in Configuration
- View OpenAPI Documentation how to link with verification rules
- Learn the complete usage of the three-stage expression in Routing
- Explore plugins how to replace the validation engine