MCPcopy Index your code
hub / github.com/Xunnamius/next-test-api-route-handler

github.com/Xunnamius/next-test-api-route-handler @v4.0.14

Chat with this repo
repository ↗ · DeepWiki ↗ · release v4.0.14 ↗ · + Follow
103 symbols 267 edges 25 files 8 documented · 8% 1 cross-repo links updated 14d agonext-test-api-route-handler@5.0.5 · 2026-05-03★ 331
What it actually does AI analysis from the code graph — generated when you open this
loading…
README

Confidently test your Next.js API routes in an isolated Next-like environment


[![Black Lives Matter!][x-badge-blm-image]][x-badge-blm-link] [![Last commit timestamp][x-badge-lastcommit-image]][x-badge-repo-link] [![Codecov][x-badge-codecov-image]][x-badge-codecov-link] [![Source license][x-badge-license-image]][x-badge-license-link] [![Uses Semantic Release!][x-badge-semanticrelease-image]][x-badge-semanticrelease-link]

[![NPM version][x-badge-npm-image]][x-badge-npm-link] [![Monthly Downloads][x-badge-downloads-image]][x-badge-npm-link]

next-test-api-route-handler

Trying to unit test your Next.js API routes? Tired of hacking something together with express or node-mocks-http or writing a bunch of boring dummy infra just to get some passing tests? And what does a "passing test" mean anyway when your handlers aren't receiving actual [NextRequest][1] objects and aren't being run by Next.js itself?

Next.js patches the global fetch function, for instance. If your tests aren't doing the same, you're making space for bugs!

Is it vexing that everything explodes when your [App Router][2] handlers call headers() or cookies() or any of the other route-specific [helper functions][3]? Or maybe you want your [Pages Router][4] handlers to receive actual [NextApiRequest][5] and [NextApiResponse][5] objects?

Sound interesting? Then want no longer! 🤩

[next-test-api-route-handler][x-badge-repo-link] (NTARH) uses Next.js's internal resolvers to precisely emulate route handling. To guarantee stability, this package is [automatically tested][6] against [each release of Next.js][7] and Node.js. Go forth and test confidently!

Note that App Router support begins with next@14.0.4 ([why?][8])


Install

npm install --save-dev next-test-api-route-handler

See [the appendix][9] for legacy Next.js support options.

Also see [the appendix][10] if you're using jest and jest-environment-jsdom.

Usage

[!IMPORTANT]

NTARH must always be the first import in your test file. This is due to the way Next.js is written and distributed. See [the appendix][11] for technical details.

// ESM
import { testApiHandler } from 'next-test-api-route-handler'; // ◄ Must be first

... all other imports ...
// CJS
const { testApiHandler } = require('next-test-api-route-handler'); // ◄ Must be first

... all other imports ...

Quick Start: App Router

/* File: test/unit.test.ts */

import { testApiHandler } from 'next-test-api-route-handler'; // ◄ Must be first import
// Import the handler under test from the app directory
import * as appHandler from '../app/your-endpoint/route';

it('does what I want', async () => {
  await testApiHandler({
    appHandler,
    // requestPatcher is optional
    requestPatcher(request) {
      request.headers.set('key', process.env.SPECIAL_TOKEN);
    },
    // responsePatcher is optional
    async responsePatcher(response) {
      const json = await response.json();
      return Response.json(
        json.apiSuccess ? { hello: 'world!' } : { goodbye: 'cruel world' }
      );
    },
    async test({ fetch }) {
      const res = await fetch({ method: 'POST', body: 'dummy-data' });
      await expect(res.json()).resolves.toStrictEqual({ hello: 'world!' }); // ◄ Passes!
    }
  });
});

Quick Start: Edge Runtime

/* File: test/unit.test.ts */

import { testApiHandler } from 'next-test-api-route-handler'; // ◄ Must be first import
// Import the handler under test from the app directory
import * as edgeHandler from '../app/your-edge-endpoint/route';

it('does what I want', async function () {
  // NTARH supports optionally typed response data via TypeScript generics:
  await testApiHandler<{ success: boolean }>({
    // Only appHandler supports edge functions. The pagesHandler prop does not!
    appHandler: edgeHandler,
    // requestPatcher is optional
    requestPatcher(request) {
      return new Request(request, {
        body: dummyReadableStream,
        duplex: 'half'
      });
    },
    async test({ fetch }) {
      // The next line would cause TypeScript to complain:
      // const { luck: success } = await (await fetch()).json();
      await expect((await fetch()).json()).resolves.toStrictEqual({
        success: true // ◄ Passes!
      });
    }
  });
});

Quick Start: Pages Router

/* File: test/unit.test.ts */

import { testApiHandler } from 'next-test-api-route-handler'; // ◄ Must be first import
// Import the handler under test and its config from the pages/api directory
import * as pagesHandler from '../pages/api/your-endpoint';

it('does what I want', async () => {
  // NTARH supports optionally typed response data via TypeScript generics:
  await testApiHandler<{ hello: string }>({
    pagesHandler,
    requestPatcher: (req) => {
      req.headers = { key: process.env.SPECIAL_TOKEN };
    },
    test: async ({ fetch }) => {
      const res = await fetch({ method: 'POST', body: 'data' });
      // The next line would cause TypeScript to complain:
      // const { goodbye: hello } = await res.json();
      const { hello } = await res.json();
      expect(hello).toBe('world'); // ◄ Passes!
    }
  });
});

API

NTARH exports a single function, testApiHandler(options), that accepts an options object as its only parameter.

At minimum, options must contain the following properties:

  • At least one of the appHandler or pagesHandler options, but not both.
  • The test option.

For example:

[!CAUTION]

Ensuring testApiHandler is imported [before][12] any Next.js package (like 'next/headers' below) is crucial to the proper function of NTARH. Doing otherwise will result in undefined behavior.

import { testApiHandler } from 'next-test-api-route-handler';
import { headers } from 'next/headers';

await testApiHandler({
  appHandler: {
    dynamic: 'force-dynamic',
    async GET(_request) {
      return Response.json(
        {
          // Yep, those fancy helper functions work too!
          hello: (await headers()).get('x-hello')
        },
        { status: 200 }
      );
    }
  },
  async test({ fetch }) {
    await expect(
      (await fetch({ headers: { 'x-hello': 'world' } })).json()
    ).resolves.toStrictEqual({
      hello: 'world'
    });
  }
});

appHandler

⪢ API reference: [appHandler][13]

The actual route handler under test (usually imported from app/*). It should be an object and/or exported module containing one or more [valid uppercase HTTP method names][14] as keys, each with an [async handling function][15] that accepts a [NextRequest][1] and ["segment data"][16] (i.e. { params }) as its two parameters. The object or module can also export [other configuration settings recognized by Next.js][17].

await testApiHandler({
  params: { id: 5 },
  appHandler: {
    async POST(request, { params: { id } }) {
      return Response.json(
        { special: request.headers.get('x-special-header'), id },
        { status: 200 }
      );
    }
  },
  async test({ fetch }) {
    expect((await fetch({ method: 'POST' })).status).toBe(200);

    const result2 = await fetch({
      method: 'POST',
      headers: { 'x-special-header': 'x' }
    });

    expect(result2.json()).toStrictEqual({ special: 'x', id: 5 });
  }
});

See also: [rejectOnHandlerError][18] and the section [Working Around Next.js fetch Patching][19].

pagesHandler

⪢ API reference: [pagesHandler][20]

The actual route handler under test (usually imported from pages/api/*). It should be an async function that accepts [NextApiRequest][5] and [NextApiResponse][5] objects as its two parameters.

await testApiHandler({
  params: { id: 5 },
  pagesHandler: (req, res) => res.status(200).send({ id: req.query.id }),
  test: async ({ fetch }) =>
    expect((await fetch({ method: 'POST' })).json()).resolves.toStrictEqual({
      id: 5
    })
});

See also: [rejectOnHandlerError][18].

test

⪢ API reference: [test][21]

An async or promise-returning function wherein test assertions can be run. This function receives one destructured parameter: fetch, which is a wrapper around Node's [global fetch][22] function. Use this to send HTTP requests to the handler under test.

[!CAUTION]

Note that fetch's resource parameter, i.e. [the first parameter in fetch(...)][23], is omitted.

⚙ Handling Redirections

Starting with version 4.0.4, NTARH sets the [fetch(...) options][24] parameter's [redirect property to 'manual'][25] by default. This prevents the WHATWG/undici fetch function from throwing a fetch failed/redirect count exceeded error.

If you want to change this value, call fetch with your own custom options parameter, e.g. fetch({ redirect: 'error' }).

⚙ Compatibility with Mock Service Worker

Starting with version 4.0.0, NTARH ships with [Mock Service Worker (msw)][26] support by adding the [x-msw-intention: "bypass"][27] and x-msw-bypass: "true" headers to all requests.

If necessary, you can override this behavior by setting the appropriate headers to some other value (e.g. "none") via fetch's customInit parameter (not requestPatcher). This comes in handy when testing functionality like [arbitrary response redirection][28] (or via the [Pages Router][29]).

For example:

import { testApiHandler } from 'next-test-api-route-handler';
import { http, passthrough, HttpResponse } from 'msw';
import { setupServer } from 'msw/node';

const server = setupServer(/* ... */);

beforeAll(() => server.listen({ onUnhandledRequest: 'error' }));

afterEach(() => {
  server.resetHandlers();
});

afterAll(() => server.close());

it('redirects a shortened URL to the real URL', async () => {
  expect.hasAssertions();

  // e.g. https://xunn.at/gg => https://www.google.com/search?q=next-test-api-route-handler
  // shortId would be "gg"
  // realLink would be https://www.google.com/search?q=next-test-api-route-handler

  const { shortId, realLink } = getUriEntry();
  const realUrl = new URL(realLink);

  await testApiHandler({
    appHandler,
    params: { shortId },
    test: async ({ fetch }) => {
      server.use(
        http.get('*', async ({ request }) => {
          return request.url === realUrl.href
            ? HttpResponse.json({ it: 'worked' }, { status: 200 })
            : passthrough();
        })
      );

      const res = await fetch({
        headers: { 'x-msw-intention': 'none' } // <==
      });

      await expect(res.json()).resolves.toMatchObject({ it: 'worked' });
      expect(res.status).toBe(200);
    }
  });
});

response.cookies

As of version 2.3.0, the response object returned by fetch() includes a non-standard cookies field containing an array of objects representing [set-cookie response header(s)][30] parsed by [the cookie package][31]. Use the cookies field to easily access a response's cookie data in your tests.

Here's an example taken straight from the [unit tests][32]:

```typescript import { testApiHandler } from 'next-test-api-route-handler';

it('handles multiple set-cookie headers', async () => { expect.hasAssertions();

await testApiHandler({ pagesHandler: (_, res) => { // Multiple calls to setHeader('Set-Cookie', ...) overwrite previous, so // we have to set the Set-Cookie header properly res .setHeader('Set-Cookie', [ serializeCookieHeader('access_token', '1234', { expires: new Date() }), serializeCookieHeader('REFRESH_TOKEN', '5678') ]) .status(200) .send({}); }, test: async ({ fetch }) => { expect((await fetch()).status).toBe(200); await expect((await fetch()).json()).resolves.toStrictEqual({}); expect((await fetch()).cookies).toStrictEqual([ { access_token: '1234', // Lowercased cookie property keys are available expires: expect.any(String), // Raw cookie property keys are also available Expires: expect.any(String)

Extension points exported contracts — how you extend this code

RunOptions (Interface)
(no doc)
test/setup.ts
NtarhInit (Interface)
(no doc)
src/index.ts
FixtureOptions (Interface)
(no doc)
test/setup.ts
NtarhInitAppRouter (Interface)
(no doc)
src/index.ts
WebpackTestFixtureOptions (Interface)
(no doc)
test/setup.ts
NtarhInitPagesRouter (Interface)
(no doc)
src/index.ts
DummyDirectoriesFixtureOptions (Interface)
(no doc)
test/setup.ts
NodeImportTestFixtureOptions (Interface)
(no doc)
test/setup.ts

Core symbols most depended-on inside this repo

testApiHandler
called by 127
src/index.ts
withMockedEnv
called by 13
test/setup.ts
run
called by 10
test/setup.ts
withMockedFixture
called by 7
test/setup.ts
writeFile
called by 6
test/setup.ts
withMockedOutput
called by 6
test/setup.ts
mkdir
called by 5
test/setup.ts
tryToRead
called by 4
spellcheck-commit.js

Shape

Function 87
Interface 12
Class 2
Method 2

Languages

TypeScript100%

Modules by API surface

test/setup.ts47 symbols
src/index.ts18 symbols
test/unit-index-imports.test.ts12 symbols
test/unit-index.test.ts9 symbols
spellcheck-commit.js8 symbols
test/unit-msw.test.ts4 symbols
external-scripts/is-next-compat.ts2 symbols
test/util.ts1 symbols
test/unit-external.test.ts1 symbols
test/integration-node.test.ts1 symbols

Used by 1 indexed graphs manifest dependencies, hub-wide

Datastores touched

(mongodb)Database · 1 repos

For agents

$ claude mcp add next-test-api-route-handler \
  -- python -m otcore.mcp_server <graph>

⬇ download graph artifact

Ask about this repo answers extend the page