Forem

Cover image for API Validation in Next.js 15 with Zod and TypeScript
Saiful Islam
Saiful Islam

Posted on

API Validation in Next.js 15 with Zod and TypeScript

In our previous blogs, we built:

  • A structured API response helperserverResponse Read here

  • An async error-handling wrapperserverAsyncResolve Read here

Now, let’s enhance API validation in Next.js 15 using Zod and TypeScript.

Why Use Zod for API Validation?

🚀 Zod is a TypeScript-first schema validation library that helps ensure API inputs are correctly formatted before processing.

Eliminates manual validation – No need for if-else checks.
Type-safe validation – Prevents unexpected runtime errors.
Structured error messages – Helps frontend developers display form errors correctly.


Step 1: Define the Validation Schema

Let’s create a validation schema for a password reset request using Zod:

import { z } from "zod";

const emailValidation = z
  .string({ required_error: "Email address is required" })
  .email({ message: "Email address is required" })
  .max(80, "Email address is too long");

export const forgetPasswordSchema = z.object({
  email: emailValidation,
});
Enter fullscreen mode Exit fullscreen mode

Explanation:

✔️ Requires an email → Users must enter a valid email address.
✔️ Ensures correct format → Rejects malformed email addresses.
✔️ Limits length → Prevents excessively long input.


Step 2: Handle API Requests with Validation

Now, let’s create a POST API route for password reset that:

  • Validates the input with Zod
  • Uses our serverAsyncResolve wrapper for error handling
  • Sends structured responses with serverResponse
import { forgetPasswordSchema } from "@/schemas/forget-password";
import { serverAsyncResolve } from "@/helpers/server-helper/serverAsyncResolve";
import { serverResponse } from "@/helpers/server-helper/serverResponse";
import { getJsonBodyData } from "@/helpers/server-helper/getJsonBodyData";

export async function POST(req: Request) {
  return serverAsyncResolve(async () => {
    const body = await getJsonBodyData<ForgetPasswordType>(req);

    const { email } = forgetPasswordSchema.parse(body);

    return serverResponse({
      success: true,
      message: "Reset password email sent",
    });
  });
}
Enter fullscreen mode Exit fullscreen mode

Uses getJsonBodyData Read previous blog to extract JSON input.

🔹 Validates input using forgetPasswordSchema.parse(body).
🔹 If validation fails, it throws a ZodError, which is handled globally.


Step 3: Handling Validation Errors Globally

Instead of handling validation errors inside each API, let’s centralize error handling in serverAsyncResolve:

import type { NextResponse } from "next/server";
import type { ServerResponseType } from "@/types";
import type { ZodError } from "zod";
import validationErrors from "@/helpers/server-helper/validationErrors";
import serverResponse from "@/helpers/server-helper/serverResponse";

export default async function serverAsyncResolve<T>(
  asyncCallback: () => Promise<NextResponse<ServerResponseType<T>>>,
): Promise<NextResponse<ServerResponseType<T>>> {
  try {
    return await asyncCallback();
  } catch (err) {
    if (err instanceof ZodError) {
      return serverResponse({
        success: false,
        error: validationErrors(err),
        status: 422,
      });
    }
    if (err instanceof Error) {
      return serverResponse({
        success: false,
        error: err.message,
        status: 500,
      });
    }
    return serverResponse({
      success: false,
      error: "Something went wrong",
      status: 500,
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

📌 This ensures all Zod validation errors return a structured response.


Step 4: Convert Zod Errors into Readable Messages

To make Zod errors more user-friendly, we transform them into an array of ValidationError objects:

import type { ValidationError } from "@/types";
import type { ZodError } from "zod";

export default function validationErrors(error: ZodError): ValidationError[] {
  return error.errors.map((err) => ({
    field: err.path.join("."),
    message: err.message,
  }));
}
Enter fullscreen mode Exit fullscreen mode

✔️ Now, frontend applications receive structured errors like:

{
  "success": false,
  "error": [
    { "field": "email", "message": "Email address is required" }
  ],
  "status": 422
}
Enter fullscreen mode Exit fullscreen mode

Final Thoughts

🎯 By combining Next.js 15, Zod, and TypeScript, we achieve:

Automatic request validation with Zod.
Centralized error handling with serverAsyncResolve.
Consistent API responses with serverResponse.

💡 Ready to implement validation in your Next.js APIs? Drop a comment and let me know how you handle validation!

Top comments (0)