Unit Test Next.js 13+ App Router API Routes with Jest and React-Testing-Library. With examples including Prisma example
Published: 2024-03-18 18:40:38

[GitHub repo](https://github.com/Dforrunner/next-with-jest) - all the examples used in this article are in this repo. I will add more examples to this repo over time.

Testing Next.js API routes seems quite daunting at first due to lack of documentation. But fear not! In this article, I'll cover how to unit test Next.js App Router API routes. Along the way I'll include some examples, tips and warnings for potential issues you may face. That said buckle up, and let's dive into the perplexing world of Next.js API route testing! 🚀

Here is what we will cover:

- Unit testing Next.js API GET route
- Unit testing Next.js API POST/PUT/DELETE routes
- Jest configurations

I'm going to assume you already have Jest set up, but if you haven't here is the link to the documentation for that: [Setting up Jest with Next.js](https://nextjs.org/docs/app/building-your-application/testing/jest)

### Unit Testing Next.js GET Route
Here is what my file structure looks like:
![File structure image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ncctujae3diuhm7jo9r4.png)

---

### 🧪 Example - basic testing

📁`app/api/items/route.ts`:
```javascript
import { NextResponse } from 'next/server';

export async function GET() {
  const items = [
    { id: 1, name: 'Item 1' },
    { id: 2, name: 'Item 2' },
  ];
  return NextResponse.json(items, { status: 200 });
}

```
This is a simple GET endpoint that will return a json object of some items. In your situation this would probably get data from a database or some other API call and then return that to the client. The last example in this article will go cover that. That said, let's look at what the test for this would look like:

📁`app/api/items/route.test.ts`:
```javascript
/**
 * @jest-environment node
 */
import { GET } from './route';

it('should return data with status 200', async () => {
  const response = await GET();
  const body = await response.json();

  expect(response.status).toBe(200);
  expect(body.length).toBe(2);
});
```
That's it. We don't actually need to mock the GET function to test it. We mainly need to mock the calls within it if needed. The last example will cover that.

-----

##### **⚠ Warning** - If you're using Jest to test React components as well read this section:

By default Jest uses the `node` environment, but when we test React components we need use a browser like environment in which case we would use the `jsdom` environment. **However**, there is a caveat and let's go over that.

We specify the environment we want to use for our test in the `jest.config.ts` file like so:

```typescript
const customJestConfig: Config = {
   testEnvironment: 'jsdom',

   //...other configs
}
```
❕ The problem is we need to use the `node` environment to test the API routes. Otherwise, our tests will fail with an error that looks something like this:

![Code snippet](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jxc8b7xk7qo10h9wrlr5.png)

This is mainly because there are some new global objects that were introduced in later versions of Node.js which are not yet in `jsdom`, such as the `Response` object. 

So, how can we use `jsdom` environment when testing React components and use the `node` environment when testing the API routes?
 
Well, all we have to do is tell Jest to use the `node` environment in the route test files, by including the following comment at the very top of the files: [read more on that here](https://jestjs.io/docs/configuration#testenvironment-string)

```javascript
/**
 * @jest-environment node
 */
```
That's it. When jest sees that it will use the node environment to run the tests in that test file. 

-----

#### Testing Response Body Schema

Let's take the previous test one step further and test the response body against a schema to ensure our API will return what we expect it to return

We will use the <a href="https://www.npmjs.com/package/jest-json-schema" target="_blank">jest-json-schema</a> package to test the schema, so let's go ahead an install that by running:

```bash
npm i jest-json-schema -D
```

If you're using TypeScript you will want to run this as well:

```bash
npm i @types/jest-json-schema -D
```

---

Ok cool, now that we have those we can use them in our tests like so:

### 🧪 Example - testing response body schema

📁`app/api/items/route.test.ts`:
```typescript
import { GET } from './route';
import { matchers } from 'jest-json-schema';
expect.extend(matchers);

it('should return data with status 200', async () => {
  const response = await GET();
  const body = await response.json();

  const schema = {
    type: 'object',
    properties: {
      id: { type: 'number' },
      name: { type: 'string' },
    },
    required: ['id', 'name'],
  };

  expect(response.status).toBe(200);
  expect(body[0]).toMatchSchema(schema);
});
```

Now we are ensuring that our API is returning the properties and the data types we expect. You can read more about <a href="https://www.npmjs.com/package/jest-json-schema#usage" target="_blank">jest-json-schema usages here</a>

--- 

Before we move onto the other API methods here is an example of a test with search query params:


### 🧪 Example - testing routes with search query params

📁`app/api/items/route.ts`:
```javascript
import { NextRequest, NextResponse } from 'next/server';

const items = [
   { id: 1, name: 'Item 1' },
   { id: 2, name: 'Item 2' },
];
 
export async function GET(req: NextRequest) {
  const itemId =  req.nextUrl.searchParams.get('Id');

  if (!itemId) {
    return NextResponse.json({ error: 'Item Id is required' }, { status: 400 });
  }

  const item = items.find((item) => item.id === parseInt(itemId));

  if (!item) {
    return NextResponse.json({ error: 'Item not found' }, { status: 404 });
  }

  return NextResponse.json(item, { status: 200 });
}

```

📁`app/api/items/route.test.ts`:
```javascript
/**
 * @jest-environment node
 */
import { GET } from './route';

it('should return data with status 200', async () => {
  const requestObj = {
    nextUrl: {
      searchParams: new URLSearchParams({ Id: '1' }),
    },
  } as any;

  const response = await GET(requestObj);
  const body = await response.json();

  expect(response.status).toBe(200);
  expect(body.id).toBe(1);
});

it('should return error with status 400 when item not found', async () => {
  const requestObj = {
    nextUrl: {
      searchParams: new URLSearchParams({ Id: '3' }),
    },
  } as any;

  const response = await GET(requestObj);
  const body = await response.json();

  expect(response.status).toBe(404);
  expect(body.error).toEqual(expect.any(String));
});
```

---

#### Unit Testing Next.js POST/PUT/DELETE/PATCH API Routes

We can apply the same strategies we discussed earlier to test these methods. The main difference here is that we'll be passing data to the route method. Testing POST, PUT, DELETE, and PATCH methods follows a similar pattern, as they essentially boil down to a POST method under the hood. Therefore, I'll only provide examples using POST, but you can test the others in a similar fashion. 

### 🧪 Example - testing POST/PUT/DELETE/PATCH methods

📁`app/api/items/route.ts`:
```javascript
import { NextRequest, NextResponse } from 'next/server';

const items = [
   { id: 1, name: 'Item 1' },
   { id: 2, name: 'Item 2' },
];
 
export async function POST(req: NextRequest) {
  const requestBody = await req.json();

  const item = {
    id: items.length + 1,
    name: requestBody.name,
  };

  items.push(item);

  return NextResponse.json(item, { status: 201 });
}
```

📁`app/api/items/route.test.ts`:
```javascript
/**
 * @jest-environment node
 */
import { POST } from './route';
it('should return added data with status 201', async () => {
  const requestObj = {
    json: async () => ({ name: 'Item 3' }),
  } as any;

  const response = await POST(requestObj);
  const body = await response.json();

  expect(response.status).toBe(201);
  expect(body.id).toEqual(expect.any(Number));
  expect(body.name).toBe('Item 3');
});
```

---
 
##### **⚠ Warning** - If you're using Module Path Aliases in your **tsconfig.json** then you need to include them in your **jest.config.ts**

If your project is using [Module Path Aliases](https://nextjs.org/docs/pages/building-your-application/configuring/absolute-imports-and-module-aliases), your gonna want to make sure your `jest.config.ts` moduleNameMapper property includes them like so:

If your `tsconfig.json` paths look something like this:
```json
{
  "compilerOptions": {
    //...
    "baseUrl": "./",
    "paths": {
      "@/prisma": ["prisma/prisma.ts"],
      "@/components/*": ["components/*"]
    }
  }
}
```

Then your `jest.config.ts` should look like this:
```json
moduleNameMapper: {
  // ...
  '^@/prisma$': '<rootDir>/prisma/prisma.ts',
  '^@/components/(.*)$': '<rootDir>/components/$1',
}
```

**💡 TIP** - However, manually updating configurations in multiple places can be a recipe for forgetfulness-induced headaches. Here's a nifty trick to programmatically ensure that your `jest.config.ts` `moduleNameMapper` stays in sync with your `tsconfig.json` paths:

We're going to use the [pathsToModuleNameMapper](https://kulshekhar.github.io/ts-jest/docs/getting-started/paths-mapping/) method from the [ts-jest](https://www.npmjs.com/package/ts-jest) package, so let's go ahead and install that by running:

```bash
npm i ts-jest -D
```

Then update the `jest.config.ts` like so:

```typescript
import { Config } from 'jest';
import { pathsToModuleNameMapper } from 'ts-jest';
import { compilerOptions } from './tsconfig.json';

const customJestConfig: Config = {
 //...other configs

  // Map TypeScript paths to Jest module names
  moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths, { prefix: '<rootDir>/' }),
};
```

---

**⚠ Warning** - To avoid sneaky bugs: when defining paths in your `tsconfig.json`, ensure they don't start with './'. I've noticed that the `pathsToModuleNameMapper` function can sometimes mishandle these paths. Forgetting to exclude them might lead to errors with some imports using aliases, while others sail smoothly, making it a real head-scratcher of a bug to tackle.

---

### 🧪 Example - testing route method that makes an external requests (In this example we will use Prisma) 

In most situations API routes will make requests to a database, or some other API. In this example we'll go over testing an API route that uses [Prisma](https://www.prisma.io/) to make database a query. 

---

##### **💡 TIP**- If you're using Prisma in your project read through the following article for a better way to mock your Prisma client: [Mocking Prisma Client](https://www.prisma.io/docs/orm/prisma-client/testing/unit-testing#mocking-prisma-client)

  
---

📁`app/api/items/route.ts`:
```javascript
import prisma from '@/prisma';
import { NextRequest, NextResponse } from 'next/server';

export async function POST(req: NextRequest) {
  try {
    //Parse the request body
    const requestBody = await req.json();

    //Validate the request body
    if (!requestBody.name) {
      return NextResponse.json({
        status: 400,
        message: 'Name is required',
      }, { status: 400 });
    }

    //Create the item
    const item = await prisma.item.create({
      data: {
        name: requestBody.name,
      },
    });

    //Return the item
    return NextResponse.json(item, { status: 201 });
    
  } catch (error) {
    //Return error response
    return NextResponse.json({
      status: 500,
      message: 'Something went wrong',
    }, { status: 500 });
  }
}
```

This route method closely mirrors real-world usage. Here is what the test's may look like:

📁`app/api/items/route.test.ts`:
```javascript
/**
 * @jest-environment node
 */
import { POST } from './route';
import prisma from '@/prisma';

// Mock prisma
// We want to ensure we're mocking the prisma client for this test
// so we don't actually make a call to the database
jest.mock('@/prisma', () => ({
  __esModule: true,
  default: {
    item: {
      create: jest.fn(),
    },
  },
}));

it('should return added data with status 201', async () => {
  const requestObj = {
    json: async () => ({ name: 'Item 3' }),
  } as any;

  // Mock the prisma client to return a value
  (prisma.item.create as jest.Mock).mockResolvedValue({ id: 2, name: 'Item 3' });

  // Call the POST function
  const response = await POST(requestObj);
  const body = await response.json();

  // Check the response
  expect(response.status).toBe(201);
  expect(body.id).toEqual(expect.any(Number));
  expect(body.name).toBe('Item 3');
  expect(prisma.item.create).toHaveBeenCalledTimes(1);
});

it('should return status 400 when name is missing from request body', async () => {
  const requestObj = {
    json: async () => ({}),
  } as any;

  (prisma.item.create as jest.Mock).mockResolvedValue({ id: 2, name: 'Item 3' });

  const response = await POST(requestObj);
  const body = await response.json();

  expect(response.status).toBe(400);
  expect(body.message).toEqual(expect.any(String));
  expect(prisma.item.create).not.toHaveBeenCalled();
});

it('should return status 500 when prisma query rejects', async () => {
  const requestObj = {
    json: async () => ({ name: 'Item 3' }),
  } as any;

  // Mock the prisma client to reject the query
  (prisma.item.create as jest.Mock).mockRejectedValue(new Error('Failed to create item'));

  const response = await POST(requestObj);
  const body = await response.json();

  expect(response.status).toBe(500);
  expect(body.message).toEqual(expect.any(String));
});

```

In the above example, I used Prisma, but you can mock anything in a similar fashion.

---

##### **💡 TIP** - Ensure you mock your network requests in your route tests. Here is why:

Avoiding running queries against our database or making external network calls during testing offers several advantages:

1. **Speed**: Mocking database queries allows tests to run much faster compared to interacting with a real database. This speed improvement is particularly significant when running a large suite of tests.
 
2. **Isolation**: Testing against a mocked database ensures that tests are isolated from changes in the actual database state. It prevents unintended side effects such as altering or deleting data crucial for other tests.

3. **Consistency**: Mocked data provides consistency in test environments, ensuring that tests produce reliable results regardless of the database's current state or external factors.
 
4. **Portability**: Tests can be executed in various environments without dependency on a specific database configuration or connection. This portability enhances the test suite's flexibility and makes it easier to run tests in different development, staging, or continuous integration environments.

5. **Cost**: Running database queries during testing might incur additional costs, especially when testing against cloud-hosted databases or services. Mocking database interactions helps reduce these expenses.

Overall, avoiding direct interaction with the database in tests promotes faster, more reliable, and cost-effective testing practices.

---

[Here is the GitHub repo](https://github.com/Dforrunner/next-with-jest) - all the examples used in this article are in this repo. I will add more examples to this repo over time. Contributions are welcomed! So feel free to clone the repo, add more examples and create a Pull Request to help your fellow developers 💯

That's a wrap for now! I hope this walkthrough has sparked some ideas for testing your Next.js API routes or has provided you with valuable insights. If you have any questions or suggestions, don't hesitate to reach out.