GuidesValidation

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.

src/modules/post/post.router.ts
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;

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.

src/modules/user/user.router.ts
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;

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
);
requiredBehavior
trueReturns 400 if no file is uploaded
falseProceeds 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.

src/modules/user/user.router.ts
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;

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.

src/modules/user/user.controller.ts
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 };