How LunaTV Handles Runtime Validation with Zod in Next.js API Routes

LunaTV validates all incoming HTTP requests at runtime using Zod schemas in its Next.js API routes, calling safeParse() to verify JSON payloads and returning structured 400 Bad Request responses when validation fails.

MoonTechLab/LunaTV leverages the Zod library for comprehensive runtime validation across its Next.js application. Every API route handler verifies incoming data against strict schemas before executing business logic. This defensive pattern ensures that only well-formed, type-safe data reaches authentication and database layers, preventing runtime crashes and providing clear error feedback to API consumers.

Defining Zod Schemas for Request Validation

Validation schemas in LunaTV are defined using Zod’s fluent API, either inline within route handlers or in dedicated modules for reuse. The codebase stores reusable schemas in src/lib/schemas/ while keeping route-specific validation close to the handler logic.

In src/lib/schemas/user.ts, the repository defines a registration schema with strict constraints:

import { z } from 'zod';

export const RegisterSchema = z.object({
  username: z.string().min(3),
  password: z.string().min(8),
  email: z.string().email(),
});

For the login endpoint in src/app/api/login/route.ts, the schema enforces minimum length requirements and custom error messages:

import { z } from 'zod';

const loginSchema = z.object({
  username: z.string().min(1, 'Username required'),
  password: z.string().min(6, 'Password too short'),
});

This approach keeps validation logic declarative and composable, allowing complex nested objects to be built from reusable schema fragments.

Runtime Validation in API Route Handlers

When a request reaches a LunaTV API endpoint, the handler immediately validates the raw request body against the predefined Zod schema. The codebase uses safeParse() rather than parse() to handle validation failures gracefully without throwing exceptions.

Using safeParse() for Defensive Validation

The safeParse() method returns an object containing either the validated data or a formatted error, enabling the handler to branch logic based on validation success:

export async function POST(req: Request) {
  const parsed = RegisterSchema.safeParse(await req.json());

  if (!parsed.success) {
    return NextResponse.json(
      { error: parsed.error.format() },
      { status: 400 }
    );
  }

  const { username, password, email } = parsed.data;
  // Continue with registration logic using type-safe values
}

This pattern appears consistently across LunaTV’s route handlers, including the login implementation in src/app/api/login/route.ts:

const result = loginSchema.safeParse(await req.json());
if (!result.success) {
  return NextResponse.json({ error: result.error.format() }, { status: 400 });
}
const { username, password } = result.data;

Alternative: Using parse() and Try-Catch

While safeParse() is preferred for explicit error handling, the codebase also demonstrates catching ZodError exceptions thrown by parse() in broader try-catch blocks that handle unexpected server errors. This dual-layer approach separates validation failures (400 responses) from server exceptions (500 responses).

Error Handling and Response Patterns

LunaTV implements a strict error handling hierarchy that distinguishes between client validation errors and server failures.

For validation failures, the handler extracts structured error messages using error.format() and returns a 400 Bad Request status:

return NextResponse.json(
  { error: parsed.error.format() },
  { status: 400 }
);

For unexpected runtime errors or database failures, the catch block logs the error (e.g., console.error('登录接口异常', error)) and returns a 500 Internal Server Error with a generic message to prevent information leakage:

} catch (error) {
  console.error('登录接口异常', error);
  return NextResponse.json({ error: '服务器错误' }, { status: 500 });
}

This separation ensures that malformed payloads receive detailed corrective feedback while server issues remain opaque to potential attackers.

Type Safety and Developer Experience

Zod provides LunaTV with static type inference, eliminating the need to maintain separate TypeScript interfaces. After calling safeParse(), the data property carries the inferred TypeScript type from the schema, giving the IDE and compiler full visibility into field types without additional type declarations.

The dependency is declared in package.json as "zod": "^3.24.1", locking the project to a stable version of the library while allowing patch updates.

Summary

  • Schema Definition: LunaTV declares Zod schemas in src/lib/schemas/ or inline within src/app/api/* route files using z.object() and chainable validators like .min() and .email().
  • Safe Parsing: Route handlers use schema.safeParse() to validate await req.json() without throwing exceptions, checking the success property before accessing data.
  • Error Responses: Validation failures trigger immediate 400 Bad Request responses containing error.format() details, while unexpected errors trigger 500 responses after logging.
  • Type Inference: Validated data objects receive automatic TypeScript typing from Zod schemas, ensuring end-to-end type safety from HTTP request to database query.

Frequently Asked Questions

What version of Zod does LunaTV use?

According to package.json, LunaTV depends on Zod version ^3.24.1. This version provides the safeParse() and format() methods used throughout the API route handlers.

Why does LunaTV use safeParse() instead of parse()?

LunaTV uses safeParse() because it returns a result object rather than throwing a ZodError on failure. This pattern allows the code to handle validation errors as control flow (returning 400 responses) without requiring try-catch blocks specifically for validation failures, while reserving exception handling for genuine server errors.

How does LunaTV structure its Zod schemas?

The codebase centralizes reusable schemas in src/lib/schemas/ (such as user.ts containing RegisterSchema), while route-specific schemas may be defined directly in the route handler file. This hybrid approach keeps common validation logic DRY while allowing endpoint-specific constraints to remain colocated with the handlers that consume them.

What happens when validation fails in LunaTV’s API routes?

When safeParse() returns success: false, the handler immediately returns a 400 Bad Request response containing the formatted Zod errors via result.error.format(). This provides API consumers with clear, field-level error messages indicating exactly which constraints failed.

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 →