All posts
Coner Murphy
Coner Murphy

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.