Standardized Response and Global Error Handling in Next.js API Routes with Prisma and Zod
Tahsin Abrar

Tahsin Abrar @tahsin000

Joined:
May 17, 2023

Standardized Response and Global Error Handling in Next.js API Routes with Prisma and Zod

Publish Date: Nov 8 '24
13 0

We'll start with helper functions for responses and error handling, then implement them in a sample route file with multiple handlers.

Image description

Objective

  1. Standardize API Responses: Ensure every API response has a consistent format:
   {
     "success": true,
     "message": "Operation completed successfully",
     "data": []
   }
Enter fullscreen mode Exit fullscreen mode
  1. Implement Global Error Handling: Catch and handle errors (validation errors via Zod, general errors) with consistent formatting, ensuring server stability on errors.

Standard Response Format

We’ll start by creating a helper function to structure our responses. This function will accept data, a message, and a status code to standardize responses.

Create a response.ts file in your lib directory:

// lib/response.ts

import { NextResponse } from "next/server";

type ApiResponse<T> = {
  success: boolean;
  message: string;
  data?: T;
};

// Helper function for successful responses
export function formatResponse<T>(data: T, message = "Operation completed successfully", status = 200) {
  return NextResponse.json<ApiResponse<T>>(
    {
      success: true,
      message,
      data,
    },
    { status }
  );
}

// Helper function for error responses
export function formatErrorResponse(message = "An error occurred", status = 500) {
  return NextResponse.json<ApiResponse<null>>(
    {
      success: false,
      message,
      data: null,
    },
    { status }
  );
}
Enter fullscreen mode Exit fullscreen mode

Global Error Handler

Next, let’s create a global error handler to catch validation errors (using Zod) and general server errors, providing consistent messaging for each type.

Create error-handler.ts in your lib directory:

// lib/error-handler.ts

import { ZodError } from "zod";
import { formatErrorResponse } from "./response";

// Handles different error types
export function routeErrorHandler(error: unknown) {
  if (error instanceof ZodError) {
    // Handling Zod validation errors
    const validationErrors = error.errors.map(err => err.message).join(", ");
    return formatErrorResponse(validationErrors, 422);
  } else if (error instanceof Error) {
    // Handling generic errors
    return formatErrorResponse(error.message, 500);
  } else {
    // Handling unknown errors
    return formatErrorResponse("An unknown error occurred", 500);
  }
}
Enter fullscreen mode Exit fullscreen mode

Example File Structure

.
├── app
│   └── api
│       └── users
│           └── route.ts
├── lib
│   ├── error-handler.ts
│   └── response.ts
└── ...
Enter fullscreen mode Exit fullscreen mode

Final Route Example

Below is a complete example of a route.ts file with multiple API operations. Each operation uses formatResponse for successful responses and routeErrorHandler for errors, following our standardized approach.

app/api/users/route.ts

// app/api/users/route.ts

import { z } from "zod";
import { PrismaClient } from "@prisma/client";
import { formatResponse, formatErrorResponse } from "@/lib/response";
import { routeErrorHandler } from "@/lib/error-handler";

const prisma = new PrismaClient();

// Shared validation schema
const userSchema = z.object({
  id: z.string().optional(),
  name: z.string().min(1, { message: "Name is required" }),
  email: z.string().email({ message: "Invalid email format" }),
});

// Insert a new user
export async function POST(req: Request) {
  try {
    const json = await req.json();
    const validatedData = userSchema.omit({ id: true }).parse(json);

    const user = await prisma.user.create({ data: validatedData });
    return formatResponse(user, "User created successfully", 201);
  } catch (error) {
    return routeErrorHandler(error);
  }
}

// Update an existing user
export async function PUT(req: Request) {
  try {
    const json = await req.json();
    const validatedData = userSchema.parse(json);

    const user = await prisma.user.update({
      where: { id: validatedData.id },
      data: validatedData,
    });
    return formatResponse(user, "User updated successfully", 200);
  } catch (error) {
    return routeErrorHandler(error);
  }
}

// Delete a user by ID
export async function DELETE(req: Request) {
  try {
    const { id } = await req.json();
    if (!id) {
      return formatErrorResponse("User ID is required", 400);
    }

    await prisma.user.delete({ where: { id } });
    return formatResponse(null, "User deleted successfully", 200);
  } catch (error) {
    return routeErrorHandler(error);
  }
}
Enter fullscreen mode Exit fullscreen mode

Explanation

  1. POST Handler (Insert):

    • Validates request data with userSchema and creates a new user.
    • Returns a success response with formatResponse.
  2. PUT Handler (Update):

    • Validates request data, including id, to update the specified user.
    • Uses formatResponse for a standardized success response.
  3. DELETE Handler (Delete):

    • Accepts an id, validates its existence, and deletes the user.
    • Uses formatResponse to indicate successful deletion or formatErrorResponse if the ID is missing.
  4. Error Handling:

    • Each handler wraps operations in a try-catch block, delegating error handling to routeErrorHandler, which processes both Zod validation errors and general errors in a consistent format.

Comments 0 total

    Add comment