How to Unit Test Next.js API Routes with Typescript
Tags: nextjs, react, typescript, api
Published: 2022-02-17 14:29:00

---
title: How to Unit Test Next.js API Routes with Typescript
published: true
date: 2022-02-16 00:00:00 UTC
tags: nextjs,react,typescript,api
canonical_url: https://www.paigeniedringhaus.com/blog/how-to-unit-test-next-js-api-routes-with-typescript
---

[![Woman coding](https://www.paigeniedringhaus.com/static/3454c5a95c805c6893c9bc924c416154/15ec7/woman-coding.jpg "Woman coding")](/static/3454c5a95c805c6893c9bc924c416154/eea4a/woman-coding.jpg)

## Introduction

**[Next.js](https://nextjs.org/)** is an awesome frontend framework. It's powered by React under the hood so it plays well with everything React has to offer out of the box: Hooks, Context, hot browser reloading, Typescript integration, and then it takes it a step further than what Create React App has, and offers even more like [routing](https://nextjs.org/docs/routing/introduction), [server side rendering (SSR)](https://nextjs.org/docs/basic-features/data-fetching/get-server-side-props), [static site generation (SSG)](https://nextjs.org/docs/basic-features/data-fetching/get-static-props), all the SEO juice that comes along with both SSR and SSG, _and_ built-in [API routing](https://nextjs.org/docs/api-routes/introduction) - no extra Node server required to proxy API calls securely to a database, another microservice, or a third party API.

At [work](https://blues.io?&utm_source=dev.to&utm_medium=web&utm_campaign=sparrow-accelerator&utm_content=unit-test-nextjs-routes), a team of developers and I have been building a **[new application](https://github.com/blues/sparrow-starter)** that we've open sourced to help our users get up and running faster with the Internet of Things (IoT) hardware we create.

For our first "accelerator application", the idea is that a user will get some of our [IoT devices](https://blues.io/products/?&utm_source=dev.to&utm_medium=web&utm_campaign=sparrow-accelerator&utm_content=unit-test-nextjs-routes), those devices will begin collecting data like temperature, humidity, motion, etc., they'll send that environmental data to a [cloud](https://notehub.io?&utm_source=dev.to&utm_medium=web&utm_campaign=sparrow-accelerator&utm_content=unit-test-nextjs-routes), and then they'll fork our "starter application" code to get a dashboard up and running, pulling in their own sensor data from the cloud, and displaying it in the browser.

To build this app, we decided to go with the Next.js framework because it offered so many of the benefits I listed above, one of the most important being the the ability to make secure API calls without having to set up a standalone Node server using Next.js's **[API routes](https://nextjs.org/docs/api-routes/introduction)**. All of the data displayed by the application must be fetched from the cloud (or a database) where the device data is stored after it's first recorded.

And this being a production-ready application, things like automated unit and end-to-end tests to ensure the various pieces of the application work as expected are a requirement - both to give the developers and our users confidence that as new features are added already existing functionality remains intact.

While by and large, the Next.js **[documentation](https://nextjs.org/docs/getting-started)** is great, one place that it does fall short is when it comes to unit testing these API routes. There is literally nothing in the documentation that touches on how to test API routes with [Jest](https://jestjs.io/) and [React Testing Library](https://testing-library.com/docs/react-testing-library/intro/) - the de facto unit testing library combo when it comes to any React-based app.

**Which is why today I'll be showing you how to unit test Next.js API routes, including gotchas like local environment variables, mocked data objects, and even Typescript types for Next-specific objects like `NextApiRequest`.**

---

## The actual Next.js API route to test

So before we get to the tests, let me give you a brief example of the sorts of API calls this application might make. For our app, the first thing that must be fetched from the cloud is info about the **"gateway devices"**.

> **Note:** The files I've linked to in the actual repo are historical links in GitHub. The project underwent a major refactor afterwards to more cleanly divide up different layers for future ease of use and flexibility, but if you dig back far enough in the commit history (or just click on the hyperlinked file name) you can see our working code that matches what I'm describing below.

### Fetch the gateway device info

The **gateways** are the brains of the operation - there are a number of sensors that all communicate with the gateways telling them what environmental readings they're getting at their various locations, and the gateways are responsible for sending that data from each sensor to the cloud - it's like a hub and spoke system you'd see on a bicycle wheel.

Before anything else can happen in the app, we have to get the gateway information, which can later be used to figure out which sensors and readings go with which gateways. I won't go into more details about how the app works because it's outside the scope of this post, but you can see the whole repo in GitHub [here](https://github.com/blues/sparrow-starter).

Let's focus on the API call going from the Next.js app to our cloud (which happens to be called [Notehub](https://notehub.io?&utm_source=dev.to&utm_medium=web&utm_campaign=sparrow-accelerator&utm_content=unit-test-nextjs-routes)). In order to [query Notehub](https://dev.blues.io/reference/notehub-api/device-api/#get-device-by-uid?&utm_source=dev.to&utm_medium=web&utm_campaign=sparrow-accelerator&utm_content=unit-test-nextjs-routes) we'll need:
* An [authorization token](https://dev.blues.io/reference/notehub-api/api-introduction/?&utm_source=dev.to&utm_medium=web&utm_campaign=sparrow-accelerator&utm_content=unit-test-nextjs-routes),
* A Notehub [project's ID](https://dev.blues.io/notehub/notehub-walkthrough/#create-a-new-project?&utm_source=dev.to&utm_medium=web&utm_campaign=sparrow-accelerator&utm_content=unit-test-nextjs-routes),
* And a gateway [device's ID](https://dev.blues.io/reference/glossary/#deviceuid?&utm_source=dev.to&utm_medium=web&utm_campaign=sparrow-accelerator&utm_content=unit-test-nextjs-routes).

Below is an example of the call made to Notehub via Next.js to fetch the gateway device data. I'll break down what's happening after the code block.

> Click the file name if you'd like to see the original code this file was modeled after.

**[`pages/api/gateways/[gatewayID].ts`](https://github.com/blues/sparrow-starter/blob/0a0c26c6e0db042b85861846defe621af999d936/src/pages/api/gateways/%5BgatewayUID%5D.ts)**

```typescript
import type { NextApiRequest, NextApiResponse } from 'next';
import axios, { AxiosResponse } from 'axios';

export default async function gatewaysHandler(
  req: NextApiRequest,
  res: NextApiResponse,
) {
  // Only allow GET requests
  if (req.method !== 'GET') {
    res.status(405).json({ err: 'Method not allowed' });
    return;
  }

  // Gateway UID must be a string
  if (typeof req.query.gatewayID !== 'string') {
    res.status(400).json({ err: 'Invalid gateway ID' });
    return;
  }

  // Query params
  const { gatewayID } = req.query;
  // Notehub values
  const { BASE_URL, AUTH_TOKEN, APP_ID } = process.env;
  // API path
  const endpoint = `${BASE_URL}/v1/projects/${APP_ID}/devices/${gatewayID}`;
  // API headers
  const headers = {
    'Content-Type': 'application/json',
    'X-SESSION-TOKEN': AUTH_TOKEN,
  };

  // API call
  try {
    const response: AxiosResponse = await axios.get(endpoint, { headers });
    // Return JSON
    res.status(200).json(response.data);
  } catch (err) {
    // Check if we got a useful response
    if (axios.isAxiosError(err)) {
      if (err.response && err.response.status === 404) {
        // Return 404 error
        res.status(404).json({ err: 'Unable to find device' });
      }
    } else {
      // Return 500 error
      res.status(500).json({ err: 'Failed to fetch Gateway data' });
    }
  }
}
```

In our code, the **[Axios HTTP library](https://axios-http.com/docs/intro)** is used to make our HTTP requests cleaner and simpler, there are **[environment variables](https://nextjs.org/docs/basic-features/environment-variables)** passed in from a `.env.local` file for various pieces of the call to the Notehub project which need to be kept secret (things like `APP_ID` and `AUTH_TOKEN`), and since this project is written in Typescript, the **[`NextApiRequest` and `NextApiResponse` types](https://nextjs.org/docs/basic-features/typescript#api-routes)** also need to be imported at the top of the file.

After the imports, there's a few validation checks to make sure that the HTTP request is a `GET`, and the `gatewayID` from the query params is a string (which it always should be, but it never hurts to confirm), then the URL request to the Notehub project is constructed (`endpoint`) along with the required `headers` to allow for access, and the call is finally made with Axios. Once the JSON payload is returned from Notehub, it is read for further errors like the gateway ID cannot be found, and if everything's in order, all the gateway info is returned.

There's just enough functionality and possible error scenarios to make it interesting, but no so much that it's overwhelming to test. Time to get on with writing unit tests.

---

## Set up API testing in Next.js

Ok, now that we've seen the actual API route we want to write unit tests for, it's time to get started. Since we're just testing API calls instead of components being rendered in the DOM, Jest is the only testing framework we'll need this time, but that being said, there's still a little extra configuration to take care of.

### Install the `node-mocks-http` Library

The first thing that we'll need to do in order to mock the HTTP requests and response objects for Notehub (instead of using actual production data, which is much harder to set up correctly every time) is to install the **[`node-mocks-http`](https://www.npmjs.com/package/node-mocks-http)**.

This library allows for mocking HTTP requests by any Node-based application that uses `request` and `response` objects (which Next.js does). It has this handy function called **[`createMocks()`](https://github.com/howardabrams/node-mocks-http#createmocks)**, which merges together two of its other functions `createRequest()` and `createResponse()` that allow us to mock both `req` and `res` objects in the same function. This lets us dictate what Notehub should accept and return when the `gatewayHandler()` function is called in our tests.

Add this library to the project's `devDependencies` list in the `package.json` file like so.

```shell
npm install --save-dev node-mocks-http
```

### Add an `.env.test.local` file for test-related environment variables

I learned the hard way that environment variables present in a Next.js project's `.env.local` file (the [prescribed way Next wants to read environment variables](https://nextjs.org/docs/basic-features/environment-variables)) do not automatically populate to its unit tests. 

Instead, we need to make a new file at the root of the project named `.env.test.local` to hold the **[test environment variables](https://nextjs.org/docs/basic-features/environment-variables#test-environment-variables)**.

This file will basically be a duplicate of the `env.local` file.

We'll include the `BASE_URL` to reach our API, a valid `AUTH_TOKEN`, a valid `APP_ID` and a valid `DEVICE_ID`. The `DEVICE_ID` is the gateway device's ID, which actually comes from the app's URL query parameters but since this is unit testing this route file's functionality, to keep all our variables in one centralized place, we'll pass the gateway's ID as an environment variable.

Here's what your test environment variables file should contain.

> **Note:** Neither this file nor your actual `.env.local` file should _ever_ be committed to your repo in GitHub. Make sure these are in your `.gitignore` file so they don't accidentally make it there where anyone could read potentially secret variables.

**`.env.test.local`**

```shell
BASE_URL=https://api.notefile.net
AUTH_TOKEN=[MY_AUTH_TOKEN]
APP_ID=[app:MY_NOTEHUB_PROJECT_ID]
DEVICE_ID=[dev:MY_GATEWAY_DEVICE_ID]
```

> **Use real env vars for your test file:** Although in our final test file you won't see us importing all of these variables to construct the Notehub URL, if they are not valid and included now, the tests will error out - the tests are actually constructing valid URLs under the hood, we're just specifying what to send the receive back when the calls are placed. Undefined variables or nonsense test data variables will cause the tests to fail.

And with those two things done, we can get to testing.

### Write the API tests

For keeping things in line with what Jest recommends, we can store all our test files inside of a folder at the root of the Next project named ` __tests__ /`, and to make it easy to figure out which tests go with which components, I tend to like to mimic the original file path and name for the file being tested.

> If you prefer to keep your tests inline with your actual source files, that's a valid choice as well. I've worked with both kinds of code repos so it's really a matter of personal preference.
> 
> In that case, I tend to just create ` __tests__ /` folders at the root of each folder alongside where the actual files live. So inside of the `pages/api/` folder I'd make a new folder named ` __tests__ /` and add any related test files in there.

Since this is a route API file buried within our `pages/` folder, I'd recommend a similar file path inside the ` __tests__ /` folder: ` __tests__ /pages/api/gateways/[gatewayID].test.ts`. In this way, a quick glance at the file name should tell us exactly what this file is testing.

Then, we come up with possible test cases to cover.

Some scenarios to test include:

- Testing a valid response from Notehub with a valid `authToken`, `APP_ID` and `DEVICE_ID` which results in a 200 status code.
- Testing that an invalid gateway ID for a device that doesn't exist and throws a 404 error.
- Testing that no gateway ID results in a 400 error.
- And testing that trying to make any type of HTTP call besides a `GET` results in a 405 error.

Below is what my tests look like to test this API endpoint. We'll dig into the details after the big code block.

> Click the file name if you'd like to see the original code this file was modeled after.

**[` __tests__ /pages/api/gateways/[gatewayUID].test.ts`](https://github.com/blues/sparrow-starter/blob/b1712d3b34040b12794ddc71671649c401450eea/ __tests__ /src/pages/api/gateways/%5BgatewayUID%5D.test.ts)**

```typescript
/**
 * @jest-environment node
 */
import { createMocks, RequestMethod } from 'node-mocks-http';
import type { NextApiRequest, NextApiResponse } from 'next';
import gatewaysHandler from '../../../../../src/pages/api/gateways/[gatewayUID]';

describe('/api/gateways/[gatewayUID] API Endpoint', () => {
  const authToken = process.env.AUTH_TOKEN;
  const gatewayID = process.env.DEVICE_ID;

  function mockRequestResponse(method: RequestMethod = 'GET') {
    const {
      req,
      res,
    }: { req: NextApiRequest; res: NextApiResponse } = createMocks({ method });
    req.headers = {
      'Content-Type': 'application/json',
      'X-SESSION-TOKEN': authToken,
    };
    req.query = { gatewayID: `${gatewayID}` };
    return { req, res };
  }

  it('should return a successful response from Notehub', async () => {
    const { req, res } = mockRequestResponse();
    await gatewaysHandler(req, res);

    expect(res.statusCode).toBe(200);
    expect(res.getHeaders()).toEqual({ 'content-type': 'application/json' });
    expect(res.statusMessage).toEqual('OK');
  });

  it('should return a 404 if Gateway UID is invalid', async () => {
    const { req, res } = mockRequestResponse();
    req.query = { gatewayID: 'hello_world' }; // invalid gateway ID

    await gatewaysHandler(req, res);

    expect(res.statusCode).toBe(404);
    expect(res._getJSONData()).toEqual({ err: 'Unable to find device' });
  });

  it('should return a 400 if Gateway ID is missing', async () => {
    const { req, res } = mockRequestResponse();
    req.query = {}; // Equivalent to a null gateway ID

    await gatewaysHandler(req, res);

    expect(res.statusCode).toBe(400);
    expect(res._getJSONData()).toEqual({
      err: 'Invalid gateway UID parameter',
    });
  });

  it('should return a 405 if HTTP method is not GET', async () => {
    const { req, res } = mockRequestResponse('POST'); // Invalid HTTP call

    await gatewaysHandler(req, res);

    expect(res.statusCode).toBe(405);
    expect(res._getJSONData()).toEqual({
      err: 'Method not allowed',
    });
  });
});
```

**Handle the imports**

Before writing our tests we need to import the `createMocks` and `RequestMethod` variables from the `node-mocks-http` library. As I noted earlier, `createMocks()` allows us to mock both the `req` and `res` objects in one function, instead of having to mock them separately.

Additionally, since this is a Typescript file, we'll need to import the `NextApiRequest` and `NextApiResponse` types from `next` - just like for the real API route file.

And finally, we need to import the real `gatewayHandler` function - it's what we're trying to unit test after all.

**Create a reusable `mockRequestResponse()` helper function**

After creating a `describe` block to house all the unit tests, I created a reusable helper function to set up the mocked API call for each test.

This reusable `mockRequestResponse()` function, allows us to only have to construct our mocked HTTP call once, cuts down on the amount of duplicate code in the test files, and makes overall readability easier. Although we may change various parts of the `req` or `res` object based on what scenario is being tested, writing this function once and being able to call it inside of each test is a big code (and time) saver.

```typescript
const authToken = process.env.AUTH_TOKEN;
const gatewayID = process.env.DEVICE_ID;

function mockRequestResponse(method: RequestMethod = 'GET') {
  const {
    req,
    res,
  }: { req: NextApiRequest; res: NextApiResponse } = createMocks({ method });
  req.headers = {
    'Content-Type': 'application/json',
    'X-SESSION-TOKEN': authToken,
  };
  req.query = { gatewayID: `${gatewayID}` };
  return { req, res };
}
```

Above, I've pulled out a snippet from the larger code block that focuses just on the `mockRequestResponse()` function and the two environment variables it needs to during its construction `authToken` and `gatewayID`. After declaring the function name we specify its method using the `node-http-mocks` `RequestMethod` object: `method:RequestMethod="GET"`, and then we destructure and set the `req` and `res` object types that come from the `createMocks()` function as `NextApiRequest` and `NextApiResponse` (just like in our real code).

We create the same `req.headers` object that Notehub requires with our test-version `authToken`, and set the mocked query parameter `gatewayID` equal to the `gatewayID` being supplied by our `.env.test.local` file.

**Write each test**

With our `mockRequestResponse()` function built, we can simply call it inside of each test to get our mocked `req` and `res` objects, call the actual `gatewayHandler()` function with those mocked objects, and make sure the responses that come back are what we expect.

If a property on the `req` object needs to be modified before the call to `gatewayHandler` is made, it's as straight forward as calling the `mockRequestResponse()` function and then modifying whatever property of the `req` object needs to be updated.

```typescript
const { req, res } = mockRequestResponse();
req.query = { gatewayID: 'hello_world' };
```

To check response objects, especially for error scenarios where different error strings are passed when a gateway ID is missing or invalid, we can use the `res._getJSONData()` function to actually read out the contents of the response. That way we can check actual error message along with the HTTP status codes.

Pretty handy, right?

---

## Check the test code coverage

If you're using **[Jest's code coverage](https://jestjs.io/docs/configuration#collectcoverage-boolean)** reporting features, now's a good time to run that function and check out the code coverage for this file in the terminal printout or the browser.

> You can open the code coverage report via the command line by typing: `open coverage/lcov-report/index.html`

And hopefully, when you navigate to the code coverage for the `pages/api/` routes, you'll see some much better code coverage for this file now.

Now go forth and add unit tests to all other API routes as needed.

---

## Conclusion

I'm a fan of the Next.js framework - it's React at its heart with lots of niceties like SEO and API routes baked in. While Next fits the bill for many projects nowadays and helps get us up and running fast with projects, its testing documentation leaves something to be desired - especially for some of its really great additions like API routes.

Automated testing is a requirement in today's modern software world, and being able to write unit tests to continue to confirm an app's functionality works as expected isn't something to be ignored or glossed over. Luckily, the `node-mocks-http` library helps make setting up mocked `req` and `res` objects simple, so that we can test our Next.js app from all angles - from presentational components in the DOM down to API routes on the backend.

Check back in a few weeks — I’ll be writing more about JavaScript, React, IoT, or something else related to web development. 

If you’d like to make sure you never miss an article I write, sign up for my newsletter here: https://paigeniedringhaus.substack.com

Thanks for reading. I hope learning how to unit test API routes helps you out in your next Next.js project (no pun intended!).

---

## References & Further Resources

- [Next.js framework](https://nextjs.org/)
- [Jest unit testing library](https://jestjs.io/)
- [React Testing Library](https://testing-library.com/docs/react-testing-library/intro/)
- [Axios HTTP library](https://axios-http.com/docs/intro) docs
- [Notehub](https://notehub.io) cloud
- [Node mocks HTTP](https://www.npmjs.com/package/node-mocks-http) library
- Full [GitHub project repo](https://github.com/blues/sparrow-starter)