Skip to main content
Version: 13.x

Validate requests

Amala accepts any Standard Schema validator directly in @Body, @Query, and @Params. Use the validation library your application already prefers—there is no Amala adapter or schema registry to configure.

Start with one order flow​

Install a Standard Schema library. This example uses Zod:

npm install zod

The controller keeps request selection, validation, the local name, and its TypeScript type together:

import {Body, Controller, Get, Params, Post, Query} from 'amala';
import {z} from 'zod';

const createOrderSchema = z.object({
sku: z.string().trim().min(1),
// The controller receives a number even when JSON contains "2".
quantity: z.coerce.number().int().positive().default(1),
});

const notifySchema = z
.enum(['true', 'false'])
.default('false')
.transform(value => value === 'true');
const orderIdSchema = z.string().uuid();

@Controller('/orders')
export class OrderController {
@Post('/')
create(
@Body(createOrderSchema) order: z.output<typeof createOrderSchema>,
@Query('notify', notifySchema) notify: boolean,
) {
// Both arguments are already validated and transformed here.
return {order, notify};
}

@Get('/:id')
getOne(@Params('id', orderIdSchema) id: string) {
return {id};
}
}

@Body(schema) validates the complete parsed body. @Query('notify', schema) and @Params('id', schema) select one value first, then validate that value. Validation still runs when a selected value is missing, allowing the schema to supply a default.

When validation succeeds, Amala injects the schema's output—not the original input. This is why coercion, trimming, defaults, and other transforms are visible in the controller.

Select one body field​

Pass the property name before the schema when a handler needs only one field:

import {Body, Controller, Patch} from 'amala';
import {z} from 'zod';

const cancelReasonSchema = z.string().trim().min(3).max(500);

@Controller('/orders')
export class OrderController {
@Patch('/:id/cancel')
cancel(
@Body('reason', cancelReasonSchema) reason: string,
) {
// The handler gets the trimmed reason, not the complete request body.
return {cancelled: true, reason};
}
}

Bring another library​

The same decorator accepts Valibot because both libraries expose the same small runtime contract:

import {Body, Controller, Post} from 'amala';
import * as v from 'valibot';

const orderSchema = v.object({
sku: v.pipe(v.string(), v.trim(), v.minLength(1)),
quantity: v.pipe(v.number(), v.integer(), v.minValue(1)),
});

@Controller('/orders')
export class OrderController {
@Post('/')
create(@Body(orderSchema) order: v.InferOutput<typeof orderSchema>) {
// No Valibot-specific Amala integration is involved.
return order;
}
}

Errors and security​

Invalid input receives 422 Unprocessable Entity. errorDetails contains normalized field paths and messages, without returning the rejected value, request body, or validator object.

A validator that throws instead of returning Standard Schema issues produces a generic 500 response. Amala does not send the thrown message to the client. Treat schema issue messages as public response text: do not put secrets or internal identifiers in custom validation messages.

Validation establishes the shape of attacker-controlled input. It does not authenticate a caller or authorize an action; keep those checks in application middleware or domain code.

OpenAPI generation​

When the validator exposes the Standard JSON Schema capability, Amala requests its openapi-3.0 input schema and uses it for generated request bodies and parameters. Zod provides this capability.

Runtime-only validators still work. If a validator cannot produce Standard JSON Schema, Amala omits those schema-derived OpenAPI details instead of inventing a contract. Set diagnostics: true during development to receive a safe warning.

Existing class-validator inputs​

Class-based validation remains supported without changes. Keep @Body() or @Body({required: true}) on a class with class-validator decorators. validatorOptions continues to apply only to that legacy path.

Use Standard Schema for new inputs when you want portable schemas, inferred input/output types, or transforms. Existing applications can migrate endpoint by endpoint; this feature does not require a flag or a major-version migration.