October 31, 2023

Unit Testing Next.js App Router API Handlers


Overview

Ever since Next.js App Router has been deemed 'production ready', I have found myself lacking valuable documentation for unit testing my code. I understand that there will be pieces of this new RSC paradigm that will require more than just unit tests; however, I would prefer many, simple unit tests for a basic GET or POST API endpoint. So, here is how I did it today - but note, I probably will continue to adapt this and make it better as I (and Next.js) improve. I'll try and keep this updated, or I will write about it again.

GET Endpoint

A GET API endpoint is actually quite simple using Next.js new App Router named functions, it essentially is just a function that returns stuff. Here is a simple example:

/app/api/user/[userId]/route.ts
import { NextResponse } from "next/server";
import UserService from "@/services/UserService";

export async function GET(
  request: Request,
  { params }: { params: { userId: string } }
) {
  const userId = params.userId;
  const user = await UserService.getUser(userId);

  return NextResponse.json(user, { status: 200 });
}
tsx

In terms of writing a test, we can treat this like a normal function, including a little bit of mocking and magic around our service and the HTTP Request and Response objects. This example below is a basic User endpoint, where the GET retrieves User data by ID.

Here is how I tested this function:

/app/api/user/[userId]/route.test.ts
/**
 * @jest-environment node
 */

import { GET } from "@/app/api/user/[userId]/route";

jest.mock("@/services/UserService", () => ({
  getUser: jest.fn().mockResolvedValue({
    id: "fake-user-id",
    name: "Will",
    email: "test@email.com",
  }),
}));

describe("api", () => {
  describe("GET", () => {
    it("retrieves user by id", async () => {
      const response = await GET({
        method: "GET",
        params: { userId: "fake-user-id" },
      } as Request);

      expect(response.ok).toBeTruthy();
      expect(await response.json()).toEqual({
        id: "fake-user-id",
        name: "Will",
        email: "test@email.com",
      });
    });
  });
});
tsx

POST Endpoint

A very similar approach can be applied to a POST endpoint. The difference: this test will accept a body and will manipulate the User, rather than retrieve it.

/app/api/user/route.ts
import { NextResponse } from "next/server";
import UserService from "@/services/UserService";

export async function POST(request: Request) {
  const userData = await request.json();

  const { id: userId } = await UserService.createUser(userData);

  return NextResponse.json(userId, { status: 201 });
}
tsx

and the test:

/app/api/user/route.test.ts
/**
 * @jest-environment node
 */

import { POST } from "@/app/api/user/route";

jest.mock("@/services/UserService", () => ({
  createUser: jest.fn().mockResolvedValue({ id: "fake-user-id" }),
}));

describe("api", () => {
  describe("POST", () => {
    it("creates a user", async () => {
      const mockBody = {
        name: "Will",
        email: "test@email.com",
      };

      const response = await POST({
        method: "POST",
        json: jest.fn().mockResolvedValue(mockBody),
      } as Request);

      expect(response.ok).toBeTruthy();
      expect(await response.json()).toEqual("Hello World");
    });
  });
});
tsx

That's all 🎉

For what it's worth, I've written these tests a variety of ways (one specifically Jest.spyOn NextResponse from "next/server") but found that this was the most intuitive, easy to read/write, and just straightforward. Hopefully this has helped and encourages you to write more tests (and advocate for testing) within the Next.js ecosystem.