Validation
Introduction
An elegant, type-safe validation library for Raptor, heavily inspired by Laravel's validation system. It uses intuitive pipe-separated rule strings ("required|string|min:8") while being fully compliant with the Standard Schema specification, enabling seamless interoperability with libraries like Zod and Valibot.
Installation
To start using the request validator, simply install into an existing Raptor application via the CLI or, if you are using Deno, import it directly from JSR.
deno add jsr:@raptor/validatorbunx jsr add @raptor/validatornpx jsr add @raptor/validatoryarn dlx jsr add @raptor/validatorpnpm dlx jsr add @raptor/validatorType inference
To get full type inference with @raptor/validator, you'll need to add type declarations to your project. This is a one-time setup that enables type-safe validation throughout your entire codebase.
Why is this needed?
JSR (JavaScript Registry) doesn't allow packages to modify global types directly. Since the validator extends the native Request object with a validate() method, we need to declare this in your project.
The good news: Once configured, you get full autocomplete and type safety everywhere you use request.validate().
Setup
In the root of your project (or in your src/ directory), create a file named types.d.ts:
// types.d.ts
import type { StandardSchemaV1 } from "@raptor/validator";
declare global {
interface Request {
validate<T>(
validator: StandardSchemaV1<T, any>,
): Promise<T>;
validateSafe<T>(
validator: StandardSchemaV1<T, any>,
): Promise<T>;
}
}
export {};Usage
Add the validation middleware to your Raptor application using the convenient helper function:
import { Context, Kernel } from "@raptor/kernel";
import validator from "@raptor/validator";
const app = new Kernel();
app.use(validator());
await app.serve();Validating a request
The validate() method will now be available as part of the context's request object.
import { Context } from "@raptor/kernel";
import { schema, rules } from "@raptor/validator";
app.use(async (context: Context) => {
const { request, response } = context;
const data = await context.request.validate(
schema({
name: rules<string>("required|string|min:2|max:50"),
email: rules<string>("required|email"),
age: rules<number>("required|integer|min:18"),
subscribed: rules<boolean>("boolean")
})
);
return data;
});Different Approaches
Raptor Validator supports three flexible approaches for defining validation schemas:
1. Pipe-Separated
The familiar Laravel approach using pipe-separated rule strings:
import { schema, rules } from "@raptor/validator";
const data = await context.request.validate(
schema({
email: rules<string>("required|email"),
password: rules<string>("required|string|min:8"),
age: rules<number>("integer|min:18")
})
);2. Array of Rules
Break rules into an array for better readability:
schema({
email: rules<string>(["required", "email"]),
password: rules<string>(["required", "string", "min:8"]),
age: rules<number>(["integer", "min:18"])
})3. Array of Validators
Use imported validator functions directly for maximum flexibility and type safety:
import {
schema,
rules,
required,
email,
string,
min,
integer
} from "@raptor/validator";
schema({
email: rules<string>([required(), email()]),
password: rules<string>([required(), string(), min(8)]),
age: rules<number>([integer(), min(18)])
});You can even mix and match approaches:
import { schema, rules, required } from "@raptor/validator";
import myCustomValidator from "./custom-validator.ts";
schema({
email: rules<string>([required(), "email"]),
password: rules<string>([required(), myCustomValidator, "min:8"])
});Using External Validators
Since Raptor Validator is Standard Schema compliant, you can use validators from other libraries like Zod or Valibot:
import { z } from "zod";
const data = await context.request.validate(
z.object({
email: z.string().email(),
age: z.number().min(18)
})
);Available Rules
Type Rules
| Rule | Description | Example |
|---|---|---|
string | Must be a string | "string" or string() |
numeric | Must be numeric | "numeric" or numeric() |
integer | Must be an integer (whole number) | "integer" or integer() |
decimal | Must be decimal (floating-point number) | "decimal" or decimal() |
boolean | Must be boolean | "boolean" or boolean() |
array | Must be an array | "array" or array() |
date | Must be a valid date | "date" or date() |
Constraint Rules
| Rule | Description | Example |
|---|---|---|
required | Field must be present and not empty | "required" or required() |
alpha | Must contain only alphabetic characters | "alpha" or alpha() |
alpha_num | Must contain only letters and numbers | "alpha_num" or alphaNum() |
alpha_dash | Must contain only letters, numbers, dashes, and underscores | "alpha_dash" or alphaDash() |
lowercase | Must be lowercase | "lowercase" or lowercase() |
uppercase | Must be uppercase | "uppercase" or uppercase() |
email | Must be a valid email address | "email" or email() |
url | Must be a valid URL | "url" or url() |
json | Must be valid JSON string | "json" or json() |
min:n | Minimum length (strings/arrays) or value (numbers) | "min:8" or min(8) |
max:n | Maximum length (strings/arrays) or value (numbers) | "max:12" or max(12) |
gt:n | Must be greater than the given value | "gt:10" or gt(10) |
gte:n | Must be greater than or equal to the given value | "gte:18" or gte(18) |
lt:n | Must be less than the given value | "lt:100" or lt(100) |
lte:n | Must be less than or equal to the given value | "lte:100" or lte(100) |
starts_with:value,... | Must start with one of the given values | "starts_with:http,https" or startsWith("http", "https") |
ends_with:value,... | Must end with one of the given values | "ends_with:.com,.org" or endsWith(".com", ".org") |
Rule Behaviour
Optional vs Required Fields
- Fields without
requiredare optional (can beundefinedornull). - Fields with
requiredmust be present and not empty. - Type rules (
string,numeric,boolean) only validate when value is present.
Error Handling
When validation fails, an UnprocessableEntity exception is thrown with detailed error messages:
import { schema, rules } from "@raptor/validator";
const data = await context.request.validate(
schema({
email: rules<string>("required|email")
})
);
// Throws a 422 status with error details:
// {
// "email": ["The email field is required"]
// }Multiple Errors Per Field
When multiple validation rules fail for a field, all error messages are returned:
import { schema, rules } from "@raptor/validator";
const data = await context.request.validate(
schema({
password: rules<string>("required|string|min:8")
})
);
// {
// "password": [
// "The password field must be a string",
// "The password field must be at least 8 in length"
// ]
// }Custom Validation Rules
Creating custom validation rules follows the Standard Schema specification. All rules must return a StandardSchemaV1 compliant validator.
Creating Simple Rules
Simple rules are functions that return a Standard Schema validator:
import type { StandardSchemaV1 } from "@raptor/validator";
export function customRule(): StandardSchemaV1<unknown> {
return {
"~standard": {
version: 1,
vendor: "raptor",
validate(value: unknown) {
if (value === undefined || value === null) {
return { value };
}
if (typeof value === "string" && value.startsWith("custom_")) {
return { value };
}
return {
issues: [{
message: "Value must start with 'custom_'",
path: []
}]
};
}
}
};
}Creating Parameterized Rules
Parameterized rules accept arguments and return validators:
import type { StandardSchemaV1 } from "@raptor/validator";
export function between(min: number, max: number): StandardSchemaV1<unknown> {
return {
"~standard": {
version: 1,
vendor: "raptor",
validate(value: unknown) {
if (value === undefined || value === null) {
return { value };
}
let isValid = false;
if (typeof value === "string") {
isValid = value.length >= min && value.length <= max;
}
if (typeof value === "number") {
isValid = value >= min && value <= max;
}
if (isValid) {
return { value };
}
return {
issues: [{
message: `Value must be between ${min} and ${max}`,
path: []
}]
};
}
}
};
}Registering Custom Rules
Global Registration (Recommended)
You can register custom validation rules (and override in-built rules) by using the configuration object as follows:
import validator from "@raptor/validator";
import { Kernel } from "@raptor/kernel";
const app = new Kernel();
app.use(validator({
rules: {
custom: customRule,
between: between,
}
}));
app.server();Or if you prefer to instantiate the validator and use methods:
import { Validator } from "@raptor/validator";
import { Kernel } from "@raptor/kernel";
const app = new Kernel();
// Get the global parser and register custom rules.
const parser = Validator.getParser();
parser.register("custom", customRule);
parser.register("between", between);
// Add the validator middleware.
app.use(validator.handle);
app.serve();Now you can use your custom rules in pipe strings anywhere in your application:
import { schema, rules } from "@raptor/validator";
const data = await context.request.validate(
schema({
code: rules<string>("required|custom"),
age: rules<number>("required|between:18,65")
})
);Using Without Registration
You can also use custom validators directly without registering them:
import { schema, rules, required } from "@raptor/validator";
import { customRule, between } from "./rules/custom.ts";
const data = await context.request.validate(
schema({
code: rules<string>([required(), customRule()]),
age: rules<number>([required(), between(18, 65)])
})
);Isolated Parser Instance
For advanced use cases where you need an isolated parser (e.g., testing or different validation contexts), you can create a separate RuleParser instance:
import { RuleParser } from "@raptor/validator";
const customParser = new RuleParser();
customParser.register("special", specialRule);
// Use this parser independently of the global one.
const validators = customParser.parse("required|special");Standard Schema Interoperability
Since Raptor Validator is fully Standard Schema compliant, you can seamlessly integrate with other validation libraries:
import { z } from "zod";
import { v } from "valibot";
import { schema, rules, required, email } from "@raptor/validator";
// Mix Raptor with Zod.
const data = await context.request.validate(
schema({
email: rules<string>([required(), email()]),
profile: z.object({
bio: z.string().max(500),
age: z.number().min(18)
})
})
);
// Mix Raptor with Valibot.
const data = await context.request.validate(
schema({
email: rules<string>("required|email"),
settings: v.object({
theme: v.picklist(["light", "dark"]),
notifications: v.boolean()
})
})
);
// Or use Zod/Valibot directly.
const data = await context.request.validate(
z.object({
email: z.string().email(),
age: z.number().min(18)
})
);