Stone.jsDocs
Paradigm

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

terminalbash
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.

app/Tasks.tsdeclarativeimperative
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.

app/pages/NewTaskPage.tsxtsx
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.

app/Tasks.tsts
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.

app/schemas.tsts
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.

app/Tasks.tsts
constructor ({ validator }) { this.validator = validator }

parse (input: unknown) {
  return this.validator.validate(input, NewTask)   // throws ValidationError (422) on mismatch
}

Stone.js

Your app exists in every runtime. Until you run it.

An open-source project by Stone Foundation
Created by Mr. Stone (Evens Pierre)