How we are testing our Next.js API Routes (Pages Router)
We all know that testing application logic is important, and the more your application scales the more it grows in importance. The last thing you want is some simple bug entering production that causes issues for your users that would’ve been easily stopped by tests had they been implemented.
So, in this post, we’re going to take a look at some patterns that we’ve found helpful when testing API routes inside a Next.js pages router application.
Testing approach
While we could test the logic contained in our API routes directly by testing the individual functions used inside the API route, I believe this is a sub-par approach. This is because if we want to perform an accurate test of our API route that will give us confidence in our code we need to test it from start to finish in one go as this is how the requests will be handled when users hit our API.
This is why I believe the better approach is to use something like
node-mocks-http
to mock the request that would be sent to the API route and
then perform our assertions against the mocked response returned to us.
Configuring this approach in your Next.js app
Now, that we know the overview of what our approach to testing the API routes will be, let’s go build an example to showcase this.
Prerequisites
Before beginning there are a couple of things I’d like to point out. First of all, as mentioned we’re going to be focusing on the pages router and not the app router. Secondly, you’ll need Jest setup and configured in your Next.js project. For brevity, I’ve omitted the complete setup as that will vary from project to project and how you want to configure your tests but Next.js has some good documentation on configuring Jest inside a Next.js project.
So, to recap, to follow along with this guide, you’ll need a Next.js pages router project setup with Jest. Once you have that, we’re ready to get started!
Making the testHandler
Because we’re going to be mocking the requests sent to our API routes, we’ll
need to install node-mocks-http
as mentioned earlier as this will allow us to
create mock request
and response
objects that can be used in the API routes
as if they were real requests/responses. To install this package run the command
npm install node-mocks-http --save-dev
in your terminal.
With node-mocks-http
installed we’re going to turn our attention to creating a
testHandler
function that will handle all of the mocking for us so then we’re
able to just pass our actual handler
function in and perform our test without
worrying about configuring the mocks each time.
To create the testHandler
function, create a new file in your project
alongside any other testing utility functions or mocks you may already have. For
example, I created the file in __tests__/_utils/test-handler.ts
. After
creating the file, add the below code to it.
// __tests__/_utils/test-handler
import type { NextApiHandler, NextApiRequest, NextApiResponse } from 'next'
import { createRequest, createResponse, RequestOptions } from 'node-mocks-http'
export type ApiRequest = NextApiRequest & ReturnType<typeof createRequest>
export type ApiResponse = NextApiResponse & ReturnType<typeof createResponse>
// We pass in the `handler` we want to test as well as an `options` object that allows us to control the request method, body, headers, etc from the test
export const testHandler = async <T>(
handler: NextApiHandler,
options: RequestOptions,
) => {
// Create the mock request and response to provide to the API route handler
const req = createRequest<ApiRequest>(options)
const res = createResponse<ApiResponse>()
// Pass the request and response into the handler and await it
await handler(req, res)
// Once the handler has finished, we'll get the data from the response using `.getJSONData()`. Here we've also asserted the data to have the type of the generic `T` this allows us to control the type of the data from the tests we write.
const data = res._getJSONData() as T
// Return the original response with the data we fetched above added in
return {
...res,
data,
}
}
With the testHandler
added, we’re almost ready to go and get started writing
tests in our app. But, first, we need to make a small tweak to our Jest config
file to ensure we exclude our new testHandler
file from Jest’s test detection
otherwise it’ll attempt to run the file and will fail because it contains no
tests.
To exclude the testHandler
file from your jest tests, you’ll want to add the
line testMatch: ["**/__tests__/**/*.test.ts"],
into your Jest config. After
adding this line it’ll ensure that Jest only looks for tests that are in files
that end with .test.ts
inside the __tests__
directory.
With that minor change out of the way, we can move on to testing an API route.
To start we’re going to look at how to test a GET
request performed to an API
route using our new testHandler
function before moving on to doing the same
with an POST
request.
GET
test
For both of our tests, I’m going to be using an example API route that would
allow users to send a GET
request to it and fetch a list of tasks or a POST
request to create a new task.
*NOTE: In this example, we’re just going to return the list of tasks from an array in the API route but in reality, you’d want to spin up a testing database and seed some data into it using a
beforeAll
step.*
Here is the example API route that would allow users to retrieve a list of tasks
when they send a GET
request to it.
// pages/api/tasks.ts
import type { NextApiRequest, NextApiResponse } from 'next'
export interface Task {
id: number
title: string
}
const tasks: Task[] = [
{ id: 123, title: 'Task One' },
{ id: 124, title: 'Task Two' },
]
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
return res.status(200).json(tasks)
}
export default handler
And then to test this API route we could write a test similar to the one below.
// __tests__/pages/api/tasks/GET.test.ts
import handler, { type Task } from '@/pages/api/tasks'
import { testHandler } from '@/__tests__/_utils/test-handler'
describe('GET /pages/api/tasks', () => {
test('returns tasks', async () => {
const { statusCode, data } = await testHandler<Task>(handler, {
method: 'GET',
})
expect(statusCode).toEqual(200)
expect(data).toEqual([
{ id: 123, title: 'Task One' },
{ id: 124, title: 'Task Two' },
])
})
})
POST
test
With the test for GET
requests now written, let’s take a look at how we can do
the same for POST
requests given the below API implementation.
// pages/api/tasks.ts
import type { NextApiRequest, NextApiResponse } from 'next'
export interface Task {
id: number
title: string
}
const tasks: Task[] = [
{ id: 123, title: 'Task One' },
{ id: 124, title: 'Task Two' },
]
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
switch (req.method) {
case 'POST':
const taskToCreate = req.body as Task
if (!taskToCreate.title) {
return res.status(400).json('Title cannot be undefined.')
}
tasks.push(taskToCreate)
return res.status(201).json(tasks)
case 'GET':
return res.status(200).json(tasks)
default:
return res.status(405).json('Method not allowed')
}
}
export default handler
With our handler
now updated to support POST
requests, let’s take a look at
the test for it.
// __tests__/pages/api/tasks/POST.test.ts
import handler, { type Task } from '@/pages/api/tasks'
import { testHandler } from '@/__tests__/_utils/test-handler'
describe('POST /pages/api/tasks', () => {
test('returns tasks', async () => {
const { statusCode, data } = await testHandler<Task>(handler, {
method: 'POST',
body: { id: 125, title: 'Task Three' },
})
expect(statusCode).toEqual(201)
// NOTE: We're testing this from the response but ideally you'd store this data in a database and retrieve it here in the test before asserting on it to ensure it was correctly stored in the database inside the handler rather than using the response data.
expect(data).toEqual([
{ id: 123, title: 'Task One' },
{ id: 124, title: 'Task Two' },
{ id: 125, title: 'Task Three' },
])
})
test('returns a 400 error when title is undefined', async () => {
const { statusCode, data } = await testHandler<string>(handler, {
method: 'POST',
body: { id: 125 },
})
expect(statusCode).toEqual(400)
expect(data).toEqual('Title cannot be undefined.')
})
})
In this file, we added two tests, one to check the success state of the API
route where a 201
is returned to us along with the newly updated task list
including our new task (see the note in the code about this).
Then we added a second test that tests whether some basic validation is working
correctly in our API route to ensure that each body sent to the endpoint as a
POST
request contains a title
property. In this test, it should return a
400
status code with a message saying Title cannot be undefined.
as we
didn’t include a title
in the body
.
Closing Thoughts
In this post, we’ve looked at how to test a Next.js API route in the pages
router for both GET
and POST
requests using node-mocks-http
and a custom
testHandler
function to allow us to easily mock requests being passed to our
API route to test the various situations you would need to handle in a real
application.
I hope you found this helpful and if you have any questions or would like to see any follow-up posts created, do let us know!
Thanks for reading.