Skip to main content

Validation

access-router validates requests before the service layer runs.

Built-in Validation

The routers validate:

  • path params such as id, subId, and field
  • query params such as page_size, include_count, include_permissions, and try_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 filter and select

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-schema objects 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:

  • create
  • update
  • upsert
  • count
  • distinct
  • advancedList
  • advancedReadFilter
  • advancedRead
  • advancedCreate
  • advancedCreateData
  • advancedUpdate
  • advancedUpdateData
  • advancedUpsert
  • advancedUpsertData
  • subList
  • subRead
  • subCreate
  • subUpdate
  • subBulkUpdate

Data routers support:

  • advancedList
  • advancedReadFilter
  • advancedRead

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 };
});