Engineering

Using Zod to validate Next.js API Route Handlers

We recently added Zod to our Next.js API route handlers to validate the request body and query parameters. Learn why and how we did this.

Using Zod to validate Next.js API Route Handlers

We're excited to share that all API routes in Dub.co now come equipped with a powerful request validation powered by Zod.

This has allowed us to catch any schema errors in run-time, providing a better developer experience for our API consumers.

In this post, we're going to take a look at how we integrated Zod to make sure API requests are validated properly.

The Problem

Have you ever accidentally sent the wrong data to the server, and the server responded with a 500 error without any helpful message?

This is a common problem when the server doesn't properly validate requests. It can be frustrating for developers who are trying to integrate with the API.

This is what we aim to solve using Zod.

What is Zod?

Zod is a TypeScript-first schema declaration and validation library. Like TypeScript catches type errors at compile time, Zod catches schema errors at runtime.

Why Zod?

Runtime validation ensures the request body and query parameters are in the expected format and structure.

Doing this manually is error-prone and not developer-friendly. This quickly becomes a maintenance nightmare as the number of route handlers grows.

This is where Zod comes into play. Zod is designed to be as developer-friendly as possible. Zod allows us to define the schema representing the request body and validate them at runtime.

Define the schema

Let's look at how we applied zod validation to the route handler responsible for creating the short links, the most critical route handler in the entire codebase.

Here, createLinkBodySchema defines the expected shape of the request body.

export const createLinkBodySchema = z.object({
  domain: z
    .string()
    .optional()
    .describe(
      "The domain of the short link. If not provided, the primary domain for the project will be used (or `dub.sh` if the project has no domains)",
    ),
  key: z
    .string()
    .optional()
    .describe(
      "The short link slug. If not provided, a random 7-character slug will be generated.",
    ),
  url: z.string().describe("The destination URL of the short link."),
  // ...
  // omitted for brevity
});

The describe method is used to provide a description for each field. Later we'll see how this description is used to generate the OpenAPI schema.

The schema above was truncated for brevity. You can refer to the full schema here.

Validate the request body

Next, we use the schema createLinkBodySchema to validate the request body in the route handler.

export async function POST(req: Request) {
  try {
    const bodyRaw = await req.json();
    const body = createLinkBodySchema.parse(bodyRaw);
    const { domain, key, url, archived } = body;
    // ... rest of the code
  } catch (error) {
    // Handle the ZodError here
  }
}

Whenever the API receives a request, it checks the request body against the schema to ensure that all the required data is present. This has proved beneficial in a number of ways.

  • Handle unexpected or invalid request body.
  • Validate the inputs for expected format and shape.
  • Provide a better developer experience by returning a friendly error message.

Returning user-friendly error messages

Zod errors are difficult for the end-users to understand. Hence, we must convert the Zod error to a user-friendly error message.

zod-error comes in handy to convert the Zod error to an easy-to-understand error message. Here is a quick example of how we use zod-error in our codebase.

import { generateErrorMessage } from "zod-error";

export function fromZodError(error: ZodError): ErrorResponse {
  return {
    error: {
      code: "unprocessable_entity",
      message: generateErrorMessage(error.issues, {
        maxErrors: 1,
        delimiter: {
          component: ": ",
        },
        path: {
          enabled: true,
          type: "objectNotation",
          label: "",
        },
        code: {
          enabled: true,
          label: "",
        },
        message: {
          enabled: true,
          label: "",
        },
      }),
    },
  };
}

zod-error comes with a number of options to customize the error message. You can find the complete list of options here.

fromZodError formats Zod Issues into a error message string that can be consumed by various clients.

It converts an array of Zod Issues that look like this:

[
  {
    validation: "url",
    code: "invalid_string",
    message: "Invalid url",
    path: ["url"],
  },
];

...into this:

url: Invalid url

This error message can be customized to suit your needs. We're still improving it to make it more user-friendly.

Auto-generating OpenAPI spec from Zod schema

We understand that manually updating the OpenAPI specification is tedious and often leads to errors and inconsistencies.

In most cases, the OpenAPI specification is not in sync with the actual request schema, making it a significant pain point for developers.

Fortunately, the Zod ecosystem has come up with a solution to this problem.

The zod-openapi package generates the OpenAPI specification automatically from the Zod schema, ensuring that the OpenAPI specification is always up-to-date.

It has been a game-changer for us as it has significantly reduced the maintenance overhead of updating the OpenAPI specification.

If you're interested in learning more about how this works, you can check out the code that generates the OpenAPI specification from the Zod schema here.

Mintlify utilises this OpenAPI specification to produce the visually appealing API documentation that enables users to fully comprehend the functionality of our platform.

Conclusion

Schema validation is an essential aspect of API development, as it ensures the consistency and integrity of data.

Zod has made this process more manageable by providing a simple and intuitive way to define and validate the schema.

Even though we are in the early stages of using Zod in our codebase, we have already noticed an improvement in the developer experience.

Furthermore, it has helped reduce the maintenance overhead of updating the OpenAPI specfification.

We're excited to continue using the powerful combination of Zod and TypeScript at Dub.

Our codebase is fully open-source, so feel free to check it out and learn more about how we use Zod in our API route handlers.

Supercharge
your marketing efforts

See why Dub.co is the link management infrastructure of choice for modern marketing teams.