metamas_

Testing Next.js API Routes

In this post, I explain the approach I found for unit testing Next.js API Routes endpoints. API Routes are a new feature, as of Next.js v9. I am using Jest to run all of the tests in my project. That includes my API request handlers/middleware, my React components, and my Mongoose models. I began setting-up my testing suite with the belief that writing unit tests for my endpoints and middleware would be the easiest part. Man, was I wrong!

The final solution I came upon is not especially novel. It just required some digging to solve. And, as these things go, it took a few wrong guesses/assumptions before I did so. It was a very frustrating couple days — honestly, more of a relief than a reward, by the end. But I poured a bunch of hours into experimenting, reading documentation, and searching/asking in forums to figure this out. So, I might as well write it down for posterity.

A brief intro to Next.js API Routes

It's quite wonderful how Next has made it so simple to create a backend API within a project. Simply create a /pages/api/ folder, and the a directory/file tree you define within it will map out the API routes. So it works the same as Next's convention for mapping your page routes based on the directory/file tree inside /pages. But instead of returning a React component for each file in /pages, you return a simple request handler for each file within /pages/api.

// pages/api/my-endpoint.js

export default (req, res) => {
  // ...
}

With this convention, it is also very straightforward to create middleware to use on your routes.

You almost certainly would use Asynchronous Functions for each handler/middleware, but I have exclude that to highlight the essential pattern.

// pages/api/my-endpoint.js

const myMiddleware = handler => {
  return (req, res) => {
    // Do middleware stuff...
    return handler(req, res)
  }
}

const myHandler = (req, res) => {
  // Do enpoint stuff...
  res.end()
}

export default myMiddleware(myHandler)

The req and res objects that Next passes to request handlers are extensions of the basic IncomingMessage and ServerResponse classes used by the http module in Node.js. They have been augmented as NextApiRequest and NextApiResponse objects, respectively, which add some nice helper methods and properties.

Testing the simplest request handler first

My simplest request handler is one that I send a GET request to, from my app's home page component. It simply responds with a status code and possibly a user object, to reflect whether or not the request came from an authenticated browser. It knows that a request came from an authenticated browser, if my authenticate middleware has attached a user object to res, and it will send that user data back as JSON. My test will mock that middleware effect.

My /pages/api/home request handler:

import authenticate from "../../middleware/authenticate"

// Make sure to export just the handler, for testing
export const handler = (req, res) => {
  const { method } = req
  const { user } = res

  try {
    switch (method) {
      case "GET":
        if (!user) return res.status(401).end()
        break
      default:
        res.setHeader("Allow", ["GET"])
        return res.status(405).end()
    }
    res.status(200).json(user)
  } catch (error) {
    console.error(error.message)
    res.status(400).end()
  }
}

export default authenticate(handler)

Notice that I am using a couple of those helper methods, res.status() and res.json(), that Next has provided for me.

Trying the easiest approach first

Given the knowledge that req and res are just extensions of classes that Node's http module uses internally, I figured to first try instantiating them to pass as arguments to my tested request handler.

My intitial /pages/api/__tests__/home.test.js unit test:

import { NextApiRequest, NextApiResponse } from "next"
import { handler } from "../home"

describe("/api/home handler", () => {
  test("responds 200 to authed GET", async () => {
    expect.assertions(1)
    let req = new NextApiRequest()
    let res = new NextApiResponse()
    res.user = { username: "scooby" }
    let response = await handler(req, res)
    expect(response.status).toBe(200)
  })

  // Another test...
})

This will not work for a couple of reasons. The first and most pressing reason is that NextApiRequest and NextApiResponse are not constructors. They are actually TypeScript types. I should have realized that from the brief example in Next's docs, but it took digging into the source for me to fully understand it. Apparently, most of Next.js core is now written in TypeScript. Damn... I really need to learn TypeScript.

Dig some more and ask for help

Realizing the above, I dug further into Next's source to find where those helper methods actually get attached. Admittedly, by this point, my brain was pretty fried and it took some guidance from the Next community for me to understand how I could use that internal utility in my tests.

This works. Use this approach. 🎉

// **pages/api/__tests__/home.test.js**

import http from "http"
import fetch from "isomorphic-unfetch"
import listen from "test-listen"
import { apiResolver } from "next-server/dist/server/api-utils"
import { handler } from "../home"

describe("/ handler", () => {
  test("responds 200 to authed GET", async () => {
    expect.assertions(1)
    let requestHandler = (req, res) => {
      res.user = { username: "scooby" }
      return apiResolver(req, res, undefined, handler)
    }
    let server = http.createServer(requestHandler)
    let url = await listen(server)
    let response = await fetch(url)
    expect(response.status).toBe(200)
    return server.close()
  })
})

This successful approach also highlights a misconception I may have had about calling the handler directly, in order to receive a response object to make assertion on. Perhaps, there are dependent behaviors met by the extra steps of creating a server, sending it a request, and receiving the response from it. That is my best guess, based on further attempts I made to simplify the above solution. (see below)

This doesn't work, but would be great if it did. 😕

// **pages/api/__tests__/home.test.js**

import { IncomingMessage, ServerResponse } from "http"
import { apiResolver } from "next-server/dist/server/api-utils"
import { handler } from "../index"

test("responds 200 to authed GET", async () => {
  expect.assertions(1)
  let req = new IncomingMessage()
  let res = new ServerResponse({ method: "GET" })
  res.user = { username: "scooby" }
  let response = await apiResolver(req, res, undefined, handler)
  expect(response.status).toBe(200)
})

I've asked for input about why the above won't work, on the Next.js Spectrum group. I'll update this post with any clarifications I get.

← back to home