Validation
access-router validates requests before the service layer runs.
Built-in Validation
The routers validate:
- path params such as
id,subId, andfield - query params such as
page_size,include_count,include_permissions, andtry_list - body shapes for list, read, create, update, upsert, distinct, count, and sub-document routes
Naming conventions:
- query-string routes use snake_case
- advanced body payloads use camelCase
- advanced body payloads use
filterandselect
Request Schemas
Model routers support requestSchemas for stricter application-level validation.
requestSchemas are validation-library agnostic for user-defined validation:
- raw Zod schemas still work
- raw
standard-schemaobjects work - custom validator functions work
- adapter objects with
validate(value)work
If you want custom OpenAPI output for a user-defined validator, wrap it with defineRequestSchema(...).
RequestSchemas Inputs
requestSchemas can be provided as:
- a raw schema object the router already understands, such as Zod or
standard-schema - a custom function returning
{ success, data }or{ success, issues } - an adapter object with
validate(value)returning the same result shape
Custom validators must return:
{ success: true, data }
or:
{
success: false,
issues: [{ message: 'Required', path: ['data', 'name'] }],
}
The router converts those issues into the standard bad-request error format.
Helper Adapters
Helpers exported by @web-ts-toolkit/access-router:
- generic adapters:
fromZod(schema),fromStandardSchema(schema) - schema/helper adapters:
fromYup(schema),fromJoi(schema),fromAjv(validate) - schema/helper adapters:
fromValibot(schema, safeParse),fromArkType(type) - schema/helper adapters:
fromIoTs(codec),fromSuperstruct(struct, validate),fromVine(validator)
defineRequestSchema(...)
Use defineRequestSchema(validator, { openapi }) when you want a custom validator and still want the generated OpenAPI document to describe that request body.
import { defineRequestSchema } from '@web-ts-toolkit/access-router';
requestSchemas: {
advancedCreate: defineRequestSchema(
async (value) => {
const body = value as { data?: { role?: string } };
if (body?.data?.role !== 'user' && body?.data?.role !== 'admin') {
return {
success: false,
issues: [{ message: 'role must be user or admin', path: ['data', 'role'] }],
};
}
return { success: true, data: body };
},
{
openapi: {
type: 'object',
properties: {
data: {
type: 'object',
properties: {
role: { type: 'string', enum: ['user', 'admin'] },
},
},
},
},
},
),
}
Without openapi metadata, custom validators still work for runtime validation, but the generated OpenAPI schema falls back to a generic object shape.
Examples
Example with standard-schema:
requestSchemas: {
advancedRead: {
'~standard': {
version: 1,
vendor: 'my-validator',
validate(value) {
const body = value as { select?: unknown };
if (body.select !== undefined && !Array.isArray(body.select)) {
return {
issues: [{ message: 'Expected array', path: ['select'] }],
};
}
return { value: body };
},
},
},
}
Example with Valibot:
import * as v from 'valibot';
import { fromValibot } from '@web-ts-toolkit/access-router';
requestSchemas: {
advancedRead: fromValibot(
v.object({
select: v.optional(v.array(v.string())),
}),
v.safeParse,
),
}
Example with ArkType:
import { type } from 'arktype';
import { fromArkType } from '@web-ts-toolkit/access-router';
requestSchemas: {
advancedList: fromArkType(
type({
'filter?': {
'public?': 'boolean',
},
}),
),
}
Example with io-ts:
import * as t from 'io-ts';
import { fromIoTs } from '@web-ts-toolkit/access-router';
requestSchemas: {
advancedRead: fromIoTs(
t.type({
select: t.union([t.undefined, t.array(t.string)]),
}),
),
}
Example with Superstruct:
import { object, optional, array, string, validate } from 'superstruct';
import { fromSuperstruct } from '@web-ts-toolkit/access-router';
const schema = object({
select: optional(array(string())),
});
requestSchemas: {
advancedRead: fromSuperstruct(schema, validate),
}
Example with Vine:
import vine from '@vinejs/vine';
import { fromVine } from '@web-ts-toolkit/access-router';
const validator = vine.create({
select: vine.array(vine.string()).optional(),
});
requestSchemas: {
advancedRead: fromVine(validator),
}
Example with a custom validator:
requestSchemas: {
advancedCreate: {
data: async (value) => {
const data = value as { role?: unknown };
if (data.role !== 'user') {
return {
success: false,
issues: [{ message: 'Invalid role', path: ['role'] }],
};
}
return { success: true, data };
},
},
}
Useful keys include:
createupdateupsertcountdistinctadvancedListadvancedReadFilteradvancedReadadvancedCreateadvancedCreateDataadvancedUpdateadvancedUpdateDataadvancedUpsertadvancedUpsertDatasubListsubReadsubCreatesubUpdatesubBulkUpdate
Data routers support:
advancedListadvancedReadFilteradvancedRead
Root Batch Validation
RootRouter validates each batch entry by operation before dispatch.
That means required fields such as id, data, field, or subId fail early with a bad request response.
Custom Routes
Use the advanced subpath when you want the same validation helpers in your own routes.
import {
parseBody,
parsePathParam,
parseQuery,
requestSchemas,
readByIdBodySchema,
} from '@web-ts-toolkit/access-router/advanced';
router.router.post('/custom/:id', async (req) => {
const id = parsePathParam(req.params.id, 'id');
const query = parseQuery(requestSchemas.readQuery, req.query);
const body = parseBody(readByIdBodySchema, req.body);
return { id, query, body };
});