How to Handle Request Validation with OmniRoute: Zod Schemas and ValidatedJsonBody

OmniRoute validates every incoming API request using Zod schemas and the validatedJsonBody helper, which returns a discriminated union containing either strongly-typed data or a pre-built 400 Bad Request response.

OmniRoute provides a robust, type-safe approach to API request validation built on Zod schemas. By leveraging the validatedJsonBody helper function located in src/shared/validation/helpers.ts, developers can enforce strict payload contracts while maintaining consistent error responses across all endpoints. This guide explains how to implement request validation in your OmniRoute API routes using the exact patterns found in the diegosouzapw/OmniRoute source code.

The Core Validation Pattern

OmniRoute centralizes request validation through a single async helper that combines JSON parsing with schema enforcement. This eliminates boilerplate and ensures uniform error handling across the entire API surface.

The ValidatedJsonBody Helper

The validatedJsonBody function in src/shared/validation/helpers.ts accepts a standard Web API Request object and a Zod schema, returning a discriminated union:

// src/shared/validation/helpers.ts
export async function validatedJsonBody<TSchema extends z.ZodTypeAny>(
  request: Request,
  schema: TSchema
): Promise<ValidatedJsonBodyResult<z.infer<TSchema>>>

The function returns one of two shapes:

  • { success: true, data } – The payload is valid and typed as z.infer<typeof schema>
  • { success: false, response } – The payload failed validation or is malformed JSON; response is a ready-to-return NextResponse with a standard error envelope

Step-by-Step Implementation

Define Your Zod Schema

All validation schemas live under src/shared/validation/schemas/ and are exported as named constants. Define the exact shape of your request payload using Zod:

// src/shared/validation/schemas/combo.ts
import { z } from "zod";

export const createComboSchema = z.object({
  name: z.string().min(1),
  targets: z.array(z.string()),
  // …other fields
});

Parse and Validate the Request Body

Import your schema and the helper into your route handler. Call validatedJsonBody at the very start of the handler:

// src/app/api/v1/combo/route.ts
import { validatedJsonBody } from "@/shared/validation/helpers";
import { createComboSchema } from "@/shared/validation/schemas/combo";

export async function POST(request: Request) {
  const parsed = await validatedJsonBody(request, createComboSchema);
  if (!parsed.success) return parsed.response;   // Returns 400 Bad Request immediately

  const { name, targets } = parsed.data;        // Fully typed as z.infer<typeof createComboSchema>
  // …business logic
}

Handle PATCH and Complex Updates

The same pattern applies to partial updates or specialized endpoints. In src/app/api/v1/settings/route.ts, the handler validates settings updates with a dedicated schema:

// src/app/api/v1/settings/route.ts
import { validatedJsonBody } from "@/shared/validation/helpers";
import { settingsUpdateSchema } from "@/shared/validation/schemas/settings";

export async function PATCH(request: Request) {
  const body = await validatedJsonBody(request, settingsUpdateSchema);
  if (!body.success) return body.response;           // 400 Bad Request

  // `body.data` is typed as the exact shape of `settingsUpdateSchema`
  await updateSettings(body.data);
  return new Response(null, { status: 204 });
}

Standard Error Response Format

When validation fails, OmniRoute returns a consistent JSON envelope that allows clients to rely on a single error-parsing routine:

{
  "error": {
    "message": "Invalid request",
    "details": [
      { "field": "name", "message": "String must contain at least 1 character(s)" },
      { "field": "targets", "message": "Expected array, received undefined" }
    ]
  }
}

This structure is automatically generated by validatedJsonBody whenever the Zod schema rejects the input or the request body contains malformed JSON. The helper returns a NextResponse with status 400 and the above payload, eliminating the need for manual error formatting in individual routes.

Middleware Integration and Execution Order

Request validation occurs at a specific point in the middleware pipeline. According to the source code in src/app/api/v1/completions/route.ts, the execution order is intentional:

  1. CORS handling
  2. Compression header processing
  3. Prompt injection guards (when applicable)
  4. validatedJsonBody validation
  5. Business logic execution

This ordering ensures that cross-cutting concerns like CORS are handled before expensive validation or processing occurs:

// src/app/api/v1/completions/route.ts (excerpt)
export async function POST(request: Request) {
  await ensureInitialized();
  const compressionRequestHeader = readCompressionRequestHeader(request);
  // …injection guard logic…
  
  const parsed = await validatedJsonBody(request, completionSchema);
  if (!parsed.success) return parsed.response;
  
  // …handleChat business logic…
}

Reusing Validation in Playground Endpoints

The pattern scales to development utilities like the playground endpoint in src/app/api/v1/playground/route.ts:

// src/app/api/v1/playground/route.ts
import { validatedJsonBody } from "@/shared/validation/helpers";
import { playgroundPromptSchema } from "@/shared/validation/schemas/playground";

export async function POST(request: Request) {
  const result = await validatedJsonBody(request, playgroundPromptSchema);
  if (!result.success) return result.response;

  const answer = await runPromptEngine(result.data);
  return new Response(JSON.stringify({ answer }), { status: 200 });
}

Summary

  • Centralized validation: OmniRoute uses the validatedJsonBody helper in src/shared/validation/helpers.ts to parse JSON and validate against Zod schemas in a single operation.
  • Discriminated unions: The helper returns either {success: true, data} with inferred types or {success: false, response} with a pre-built 400 error.
  • Schema location: All Zod schemas reside in src/shared/validation/schemas/ and are imported as named exports into route handlers.
  • Consistent errors: Failed validations automatically return a standardized error envelope with field-level details, ensuring uniform API contracts.
  • Pipeline positioning: Validation executes after CORS, compression, and security guards but before business logic, as shown in src/app/api/v1/completions/route.ts.

Frequently Asked Questions

What validation library does OmniRoute use?

OmniRoute uses Zod for runtime type validation. Every request body is validated against Zod schemas defined in src/shared/validation/schemas/, providing both type safety at build time and runtime data integrity.

Where are validation schemas stored in OmniRoute?

All validation schemas are stored in the src/shared/validation/schemas/ directory. Each domain entity (combos, settings, completions, playground) has its own file exporting named schema constants like createComboSchema or settingsUpdateSchema.

How does validatedJsonBody handle malformed JSON?

If the request body contains malformed JSON or fails Zod validation, validatedJsonBody returns { success: false, response } where response is a NextResponse with status 400 and a standard error envelope containing specific field-level error messages.

Can I use validatedJsonBody with Next.js App Router?

Yes. The validatedJsonBody function is designed specifically for Next.js App Router API routes. It accepts the standard Web API Request object provided by Next.js route handlers and returns responses compatible with the NextResponse type used throughout the App Router.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →