Usage
Validate request data inputs such as req.body, req.query, and req.params declaratively on any route — no middleware boilerplate, no manual error handling. Drop a Zod schema or class-validator DTO into the validation config, and Arkos handles the rest: error responses and automatic OpenAPI spec generation.
This validation system works the same way across both ArkosRouter and RouteHook.
Request Body Validation
Applied on routes that receive a request body — typically POST, PUT, and PATCH.
import { ArkosRouter } from "arkos";
import z from "zod";
import postController from "./post.controller";
const router = ArkosRouter();
const CreatePostSchema = z.object({
title: z.string().min(1),
content: z.string().min(1),
published: z.boolean().optional(),
authorId: z.string().uuid(),
});
router.post(
{
path: "/api/posts",
validation: {
body: CreatePostSchema,
},
},
postController.createPost
);
export default router;import { ArkosRouter } from "arkos";
import { IsString, IsBoolean, IsUUID, IsOptional, MinLength } from "class-validator";
import postController from "./post.controller";
const router = ArkosRouter();
class CreatePostDto {
@IsString()
@MinLength(1)
title: string;
@IsString()
@MinLength(1)
content: string;
@IsBoolean()
@IsOptional()
published?: boolean;
@IsUUID()
authorId: string;
}
router.post(
{
path: "/api/posts",
validation: {
body: CreatePostDto,
},
},
postController.createPost
);
export default router;import { ArkosRouter, RouteHook } from "arkos";
import z from "zod";
const CreatePostSchema = z.object({
title: z.string().min(1),
content: z.string().min(1),
published: z.boolean().optional(),
authorId: z.string().uuid(),
});
export const hook: RouteHook = {
createOne: {
validation: {
body: CreatePostSchema,
},
},
};
const router = ArkosRouter();
export default router;import { ArkosRouter, RouteHook } from "arkos";
import { IsString, IsBoolean, IsUUID, IsOptional, MinLength } from "class-validator";
class CreatePostDto {
@IsString()
@MinLength(1)
title: string;
@IsString()
@MinLength(1)
content: string;
@IsBoolean()
@IsOptional()
published?: boolean;
@IsUUID()
authorId: string;
}
export const hook: RouteHook = {
createOne: {
validation: {
body: CreatePostDto,
},
},
};
const router = ArkosRouter();
export default router;RouteHook is the new name for export const config: RouterConfig. If you have existing code using the old name it still works but will log a deprecation warning. See Route Hook for full details.
Validation error response:
{
"status": "error",
"message": "Invalid Data",
"code": 400,
"errors": [
{
"property": "authorId",
"constraints": {
"isUuid": "authorId must be a valid UUID"
}
}
]
}Request Query & Params Validation
Applied on routes that receive URL query strings or path parameters.
Query and params values arrive as strings from the URL. Use z.coerce or @Type() to cast them to the correct type — or use the CLI code generation which handles this automatically.
import { ArkosRouter } from "arkos";
import z from "zod";
import userController from "./user.controller";
const router = ArkosRouter();
router.get(
{
path: "/api/users",
validation: {
query: z.object({
role: z.enum(["admin", "user"]).optional(),
active: z.coerce.boolean().optional(),
limit: z.coerce.number().int().min(1).max(100).optional(),
}),
},
},
userController.getUsers
);
router.get(
{
path: "/api/users/:id",
validation: {
params: z.object({
id: z.string().uuid("Invalid user ID"),
}),
},
},
userController.getUser
);
router.patch(
{
path: "/api/users/:id",
validation: {
params: z.object({
id: z.string().uuid(),
}),
body: z.object({
name: z.string().min(1).optional(),
email: z.string().email().optional(),
}),
query: z.object({
notify: z.coerce.boolean().optional(),
}),
},
},
userController.updateUser
);
export default router;import { ArkosRouter } from "arkos";
import { IsEnum, IsBoolean, IsInt, IsUUID, IsString, IsEmail, IsOptional, Min, Max } from "class-validator";
import { Type } from "class-transformer";
import userController from "./user.controller";
const router = ArkosRouter();
class UserQueryDto {
@IsEnum(["admin", "user"])
@IsOptional()
role?: string;
@Type(() => Boolean)
@IsBoolean()
@IsOptional()
active?: boolean;
@Type(() => Number)
@IsInt()
@Min(1)
@Max(100)
@IsOptional()
limit?: number;
}
class UserParamsDto {
@IsUUID()
id: string;
}
class UpdateUserBodyDto {
@IsString()
@IsOptional()
name?: string;
@IsEmail()
@IsOptional()
email?: string;
}
class UpdateUserQueryDto {
@Type(() => Boolean)
@IsBoolean()
@IsOptional()
notify?: boolean;
}
router.get(
{
path: "/api/users",
validation: { query: UserQueryDto },
},
userController.getUsers
);
router.get(
{
path: "/api/users/:id",
validation: { params: UserParamsDto },
},
userController.getUser
);
router.patch(
{
path: "/api/users/:id",
validation: {
params: UserParamsDto,
body: UpdateUserBodyDto,
query: UpdateUserQueryDto,
},
},
userController.updateUser
);
export default router;import { ArkosRouter, RouteHook } from "arkos";
import z from "zod";
export const hook: RouteHook = {
findMany: {
validation: {
query: z.object({
published: z.coerce.boolean().optional(),
limit: z.coerce.number().int().min(1).max(100).optional(),
}),
},
},
updateOne: {
validation: {
body: z.object({
title: z.string().min(1).optional(),
content: z.string().min(1).optional(),
}),
query: z.object({
notify: z.coerce.boolean().optional(),
}),
},
},
};
const router = ArkosRouter();
export default router;import { ArkosRouter, RouteHook } from "arkos";
import { IsBoolean, IsInt, IsString, IsOptional, Min, Max, MinLength } from "class-validator";
import { Type } from "class-transformer";
class PostQueryDto {
@Type(() => Boolean)
@IsBoolean()
@IsOptional()
published?: boolean;
@Type(() => Number)
@IsInt()
@Min(1)
@Max(100)
@IsOptional()
limit?: number;
}
class UpdatePostBodyDto {
@IsString()
@MinLength(1)
@IsOptional()
title?: string;
@IsString()
@MinLength(1)
@IsOptional()
content?: string;
}
class UpdatePostQueryDto {
@Type(() => Boolean)
@IsBoolean()
@IsOptional()
notify?: boolean;
}
export const hook: RouteHook = {
findMany: {
validation: {
query: PostQueryDto,
},
},
updateOne: {
validation: {
body: UpdatePostBodyDto,
query: UpdatePostQueryDto,
},
},
};
const router = ArkosRouter();
export default router;RouteHook is the new name for export const config: RouterConfig. If you have existing code using the old name it still works but will log a deprecation warning. See Route Hook for full details.
Validation error response:
{
"status": "error",
"message": "Invalid Data",
"code": 400,
"errors": [
{
"property": "id",
"constraints": {
"isUuid": "id must be a valid UUID"
}
}
]
}Validation With File Uploads
When combining validation with file uploads, only pass text fields to validation.body — file fields are handled separately by the uploads config.
router.post(
{
path: "/api/users/:id/avatar",
validation: {
params: z.object({ id: z.string().uuid() }),
body: z.object({ caption: z.string().optional() }),
// no avatar field here — handled by uploads
},
experimental: {
uploads: { type: "single", field: "avatar", required: true },
},
},
userController.uploadAvatar
);import { IsUUID, IsString, IsOptional } from "class-validator";
class UploadAvatarParamsDto {
@IsUUID()
id: string;
}
class UploadAvatarBodyDto {
@IsString()
@IsOptional()
caption?: string;
}
router.post(
{
path: "/api/users/:id/avatar",
validation: {
params: UploadAvatarParamsDto,
body: UploadAvatarBodyDto,
// no avatar field here — handled by uploads
},
experimental: {
uploads: { type: "single", field: "avatar", required: true },
},
},
userController.uploadAvatar
);import { ArkosRouter, RouteHook } from "arkos";
import z from "zod";
export const hook: RouteHook = {
uploadFile: {
validation: {
body: z.object({ caption: z.string().optional() }),
// no file field here — handled by uploads
},
},
};
const router = ArkosRouter();
export default router;import { ArkosRouter, RouteHook } from "arkos";
import { IsString, IsOptional } from "class-validator";
class UploadFileBodyDto {
@IsString()
@IsOptional()
caption?: string;
}
export const hook: RouteHook = {
uploadFile: {
validation: {
body: UploadFileBodyDto,
// no file field here — handled by uploads
},
},
};
const router = ArkosRouter();
export default router;RouteHook is the new name for export const config: RouterConfig. If you have existing code using the old name it still works but will log a deprecation warning. See Route Hook for full details.
required | Behavior |
|---|---|
true | Returns 400 if no file is uploaded |
false | Proceeds without a file |
See File Upload guide for full configuration.
Accessing Validated Data
Arkos automatically types req.body, req.query, and req.params from your validation schema — every handler and middleware in the route stack shares the same typed req with no manual generic declarations needed.
import { ArkosRouter } from "arkos";
import z from "zod";
import userController from "@/src/modules/user/user.controller";
const router = ArkosRouter();
const UpdateUserBody = z.object({
name: z.string().min(1).optional(),
email: z.string().email().optional(),
});
const UpdateUserParams = z.object({
id: z.string().uuid(),
});
const UpdateUserQuery = z.object({
notify: z.coerce.boolean().optional(),
});
router.patch(
{
path: "/api/users/:id",
validation: {
params: UpdateUserParams,
body: UpdateUserBody,
query: UpdateUserQuery,
},
},
logMiddleware, // req.params, req.body, req.query all typed here
userController.updateOne // and here — same signature, no extra work
);
export default router;import { ArkosRouter } from "arkos";
import { IsString, IsEmail, IsBoolean, IsUUID, IsOptional } from "class-validator";
import { Type } from "class-transformer";
import userController from "@/src/modules/user/user.controller";
const router = ArkosRouter();
class UpdateUserBodyDto {
@IsString()
@IsOptional()
name?: string;
@IsEmail()
@IsOptional()
email?: string;
}
class UpdateUserParamsDto {
@IsUUID()
id: string;
}
class UpdateUserQueryDto {
@Type(() => Boolean)
@IsBoolean()
@IsOptional()
notify?: boolean;
}
router.patch(
{
path: "/api/users/:id",
validation: {
params: UpdateUserParamsDto,
body: UpdateUserBodyDto,
query: UpdateUserQueryDto,
},
},
logMiddleware,
userController.updateOne
);
export default router;import { ArkosRouter, RouteHook } from "arkos";
import z from "zod";
const UpdateMeBody = z.object({
name: z.string().min(1).optional(),
email: z.string().email().optional(),
});
export const hook: RouteHook = {
updateMe: {
validation: {
body: UpdateMeBody,
},
},
};
const router = ArkosRouter();
export default router;import { ArkosRequest, ArkosResponse, NextFunction } from "arkos";
import { z } from "zod";
const UpdateMeBody = z.object({
name: z.string().min(1).optional(),
email: z.string().email().optional(),
});
type UpdateMeBody = z.infer<typeof UpdateMeBody>;
export const beforeUpdateMe = (
req: ArkosRequest<any, any, UpdateMeBody>,
res: ArkosResponse,
next: NextFunction
) => {
const { name, email } = req.body; // typed, validated
next();
};import { ArkosRouter, RouteHook } from "arkos";
import { IsString, IsEmail, IsOptional } from "class-validator";
class UpdateMeBodyDto {
@IsString()
@IsOptional()
name?: string;
@IsEmail()
@IsOptional()
email?: string;
}
export const hook: RouteHook = {
updateMe: {
validation: {
body: UpdateMeBodyDto,
},
},
};
const router = ArkosRouter();
export default router;import { ArkosRequest, ArkosResponse, NextFunction } from "arkos";
export const beforeUpdateMe = (
req: ArkosRequest<any, any, UpdateMeBodyDto>,
res: ArkosResponse,
next: NextFunction
) => {
const { name, email } = req.body; // typed, validated
next();
};RouteHook is the new name for export const config: RouterConfig. If you have existing code using the old name it still works but will log a deprecation warning. See Route Hook for full details.
import { ArkosRequest, ArkosResponse } from "arkos";
const updateOne = async (req: ArkosRequest, res: ArkosResponse) => {
const { id } = req.params; // string, validated UUID
const { notify } = req.query; // boolean, coerced
const { name, email } = req.body; // typed, validated
};
export default { updateOne };