Extensions
Validation
Validation is where backend and frontend stop duplicating work. Write the shape of the data once; enforce it on the route that accepts it and on the form that produces it. Drift becomes impossible because there is only one schema.
#Install
npm i @stone-js/validation#Validate at the boundary
The principle
A boundary should reject malformed input before it reaches the domain, so the domain can assume its inputs are well-formed. Validation belongs at the edge, as a gate, not scattered through business logic.
In Stone.js
validate(rules) is middleware: it checks the event against a schema and rejects with a 422 before your handler runs. validateEvent(event, rules) does the same inline. Both accept any Standard Schema or Zod-like schema.
import { z } from 'zod'
import { validate } from '@stone-js/validation'
import { EventHandler, Post } from '@stone-js/router'
export const NewTask = z.object({ title: z.string().min(1).max(120) })
@EventHandler('/tasks')
export class TaskController {
@Post('/', { middleware: [validate({ body: NewTask })] })
create (event) {
return this.tasks.add(event.get('body')) // reaches here only if body matched NewTask
}
}import { z } from 'zod'
import { validateEvent } from '@stone-js/validation'
export const NewTask = z.object({ title: z.string().min(1).max(120) })
const create = ({ tasks }) => (event) => {
validateEvent(event, { body: NewTask }) // throws ValidationError (422) on mismatch
return tasks.add(event.get('body'))
}#The same schema on the frontend
Because the schema is a plain value, the form that creates a task validates against the exact object the API enforces.
import { NewTask } from '../Tasks'
const result = NewTask.safeParse(formValues)
if (!result.success) setErrors(result.error.issues)One schema. It guards the route and shapes the form. They can never disagree.
#Rules and sources
Rules are a map from a source (body, query, params) to a schema, so one middleware can validate several parts of an event at once.
validate({
params: z.object({ id: z.string().uuid() }),
query: z.object({ page: z.coerce.number().default(1) }),
body: NewTask
})#Bring any schema
Validation speaks the Standard Schema interface, so Zod, Valibot, ArkType and others work as is. For a library that is not yet Standard Schema, adapt it explicitly with fromZod or fromStandard; the rest of your code stays the same.
import { fromZod, fromStandard } from '@stone-js/validation'
const NewTask = fromZod(zodSchema) // wrap a Zod schema
const Filter = fromStandard(anyStandard) // wrap any Standard Schema#The failure shape
A failed check throws a ValidationError the kernel maps to 422, with the issues attached, so clients get a precise, structured error without you writing the plumbing. Need it inline instead of as middleware? validateEvent throws the same error; or resolve the Validator service to validate arbitrary values.
constructor ({ validator }) { this.validator = validator }
parse (input: unknown) {
return this.validator.validate(input, NewTask) // throws ValidationError (422) on mismatch
}