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:
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 });
}
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:
/**
* @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",
});
});
});
});
#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.
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 });
}
and the test:
/**
* @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");
});
});
});
#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.