How to validate your Next.js API with Zod and TypeScript

Learn how to validate Next.js API inputs with Zod and TypeScript to ensure secure, reliable, and type-safe APIs. Step-by-step guide with code examples included.

 · 

5 min read

notion-image
Next.js is a popular React framework known for building end-to-end web applications. It enables developers to manage both the front-end and back-end of a project within a unified codebase, simplifying development and maintenance.
When building web APIs, input validation becomes crucial because your routes may be consumed by various clients. These clients could range from the React UI you’re developing to external systems accessing your public APIs. Ensuring that your API only processes valid data is important for maintaining security and reliability, especially when inputs come from sources beyond your control.
Zod is a popular library in the JavaScript and TypeScript ecosystem for schema validation. It offers an intuitive and expressive API, allowing you to define the structure (or schema) of your data and enforce validation rules. Zod is widely integrated with other tools and libraries, such as react-hook-form for form handling, tRPC for building type-safe APIs, and zod-to-json-schema for generating JSON Schema definitions, making it a versatile choice for developers.

Why Validate API Inputs?

Input validation is important for enhancing the reliability and security of your software. It helps safeguard your application by preventing common vulnerabilities like injection attacks, cross-site scripting (XSS), and denial-of-service (DoS) exploits.
Beyond security, validation also prevents application errors caused by invalid or unsupported data. Without proper checks, your API might encounter unexpected values or incorrect types, leading to runtime errors, application crashes, or unpredictable behavior. Ensuring only valid data reaches your code minimizes these risks and keeps your application running smoothly.
 

How can I use Zod?

The key features of Zod are schema declaration, parsing, and type inference. Let’s see how to use Zod and how to leverage them.
 
Let’s start by installing zod into your project.
Bash
npm install zod
 
With Zod we can create the schema definition of an object, like this one:
 
JavaScript
import { z } from 'zod';

const userSchema = z.object({
  name: z.string().min(1, 'Name is required'),
  email: z.string().email('Invalid email format'),
  age: z.number().int().gte(18, 'Must be 18 or older'),
});
 
This code defines the schema of an object with a mandatory name property, a valid email address, and a numeric integer value age, greater than or equals to 18.
 
With Zod you can also define the error message when the value provided doesn’t satisfy the conditions.
 
Now that we have a schema definition, let’s parse an object to validate it.
JavaScript
const user = {
	name: 'Luca',
	email: '[email protected]',
	age: 38
}

const validatedUser = userSchema.parse(user)
 
If the object provided is not correct, .parse() throws an error.
If you want to avoid throwing an error, you can use .safeParse() that returns an object instead:
TypeScript
userSchema.safeParse(12);
// => { success: false; error: ZodError }

userSchema.safeParse(user);
// => { success: true; data: { ...user } }
 
Finally, you can infer the TypeScript types from your schema!
TypeScript
type User = z.infer<typeof userSchema>;
 

How to use Zod in my Next.js App Route

Now, let’s create a Next.js API POST route that receives a body payload that we want to validate.
TypeScript
export async function POST(request: NextRequest) {
  try {
    // Parse the request body
    const body = await request.json();
    const data = userSchema.parse(body);

    // Business logic here (e.g., save to database)
    return NextResponse.json(
      { message: 'Registration successful', data },
      { status: 200 }
    );
  } catch (error) {
    if (error instanceof z.ZodError) {
      // Return validation errors
      return NextResponse.json(
        { message: 'Validation error', errors: error.errors },
        { status: 400 }
      );
    }

    // Handle unexpected errors
    return NextResponse.json(
      { message: 'Something went wrong', error: error.message },
      { status: 500 }
    );
  }
}
 
This POST method receives a JSON payload, it reads and parses it through the Zod schema that we defined.
If the validation succeeds, we can process the data, otherwise we return a validation error.
This way we can ensure that our business logic uses only valid data.
 
Zod works particularly well with Next.js, because you can define the Schema for the data expected on the API routes, and reuse the same schema and types when you need to build the same object on the frontend, to perform the request.
 
Here’s an example of a React component that sends a POST request, using userSchema to validate the values provided by the user in a form.
 
TypeScript
import React, { useState } from 'react';

// import the schema and inferred type created with Zod
// 👇👇👇
import { userSchema, type User } from '../schemas/userSchema';

const RegisterForm: React.FC = () => {
  const [formData, setFormData] = useState<User>({
    name: '',
    email: '',
    age: 18, // Default age
  });

  const [error, setError] = useState<string | null>(null);
  const [success, setSuccess] = useState<string | null>(null);

  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const { name, value } = e.target;

    // Update state dynamically based on input name
    setFormData((prev) => ({
      ...prev,
      [name]: name === 'age' ? Number(value) : value,
    }));
  };

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();

    try {
      // Validate the form data using the Zod schema
      // 👇👇👇
      userSchema.parse(formData);

      // Send the POST request
      const response = await fetch('/api/register', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify(formData),
      });

      if (response.ok) {
        setSuccess('Registration successful!');
        setError(null);
      } else {
        const errorResponse = await response.json();
        setError(errorResponse.message || 'Failed to register');
        setSuccess(null);
      }
    } catch (err: any) {
      if (err.errors) {
        // get all errors from Zod, and concatenate them in a string
        setError(err.errors.map((error: any) => error.message).join(', '));
      } else {
        setError('An unexpected error occurred');
      }
      setSuccess(null);
    }
  };

  return (
    <div>
      <h1>Register</h1>
      {error && <p style={{ color: 'red' }}>{error}</p>}
      {success && <p style={{ color: 'green' }}>{success}</p>}
      <form onSubmit={handleSubmit}>
        <div>
          <label>
            Name:
            <input
              type="text"
              name="name"
              value={formData.name}
              onChange={handleChange}
            />
          </label>
        </div>
        <div>
          <label>
            Email:
            <input
              type="email"
              name="email"
              value={formData.email}
              onChange={handleChange}
            />
          </label>
        </div>
        <div>
          <label>
            Age:
            <input
              type="number"
              name="age"
              value={formData.age}
              onChange={handleChange}
            />
          </label>
        </div>
        <button type="submit">Submit</button>
      </form>
    </div>
  );
};

export default RegisterForm;
 
To test your validation, you can use cURL in the terminal or Postman if you prefer to use a user interface.
 
With cURL:
TypeScript
curl -X POST http://localhost:3000/api/register \
-H "Content-Type: application/json" \
-d '{"name": "Luca", "email": "[email protected]", "age": 38}'
 

Conclusions

I encourage you to use Zod extensively in your codebase, and to use it to define your types whenever you need validation or when you expose an interface to another client of your architecture, like a private or public API.
 
Zod is extremely useful and expressive, it allows you to define complex data types and validation rules. You can read the full documentation on GitHub.
 
I hope this article was useful, and helped you level up your coding skills.
 
Cheers, Luca

Ship your startup in days and start selling.

Save weeks of coding with the #1 Next.js Startup and Chrome Extension Boilerplate. Made by a maker for makers.