next-test-api-route-handler (NTARH)

repository·main·Indexed 43 days ago

https://github.com/xunnamius/next-test-api-route-handler

A utility for unit and integration testing Next.js API routes and handlers in an isolated environment. It emulates the Next.js route handling environment using internal resolvers to provide high-fidelity testing for both App Router and Pages Router, including support for Edge Runtime. Version 5.0.7.

Tokens
11.2K
Snippets
34
Records
45
Agent score
55%

What's inside next-test-api-route-handler

  1. Understand the limitations of App Router and Edge Runtime emulation

    main

    NTARH is designed for unit testing API route handlers, not for recreating the full Next.js runtime. Because of this, several Next.js features are not available:

    • Middleware and NextResponse.next()
    • Metadata, Static Assets, and Styling
    • OpenTelemetry, Instrumentation, and Caching
    • Server Actions and Mutations
    • React components
    • Helper functions (except: cookies, fetch (global), headers, NextRequest, NextResponse, notFound, permanentRedirect, redirect, and userAgent)

    Important Runtime Note: While NTARH provides support for the "edge runtime", your tests will always run in Node.js (or your chosen test runner). It does not run in a different runtime, realm, or VM. NTARH should be used for unit testing handlers in addition to, not instead of, end-to-end testing.

  2. Important: Import order for NTARH

    main

    To ensure Next.js internal resolvers work correctly, next-test-api-route-handler must always be the first import in your test file.

    ESM usage:

    import { testApiHandler } from 'next-test-api-route-handler'; // ◄ Must be first
    import * as myHandler from '../app/api/route';

    CJS usage:

    const { testApiHandler } = require('next-test-api-route-handler'); // ◄ Must be first
    const myHandler = require('../app/api/route');

    If you use automated import sorting (like eslint-plugin-import), you can import NTARH as a side-effect first to satisfy the requirement:

    import 'next-test-api-route-handler';
    
    // ... other imports ...
    import { testApiHandler } from 'next-test-api-route-handler';
    import { testApiHandler } from 'next-test-api-route-handler'; // ◄ Must be first
    
    ... all other imports ...
  3. Patch request and response objects in Pages Router tests

    main

    To simulate specific request or response states in a Pages Router test, use the requestPatcher and responsePatcher properties within the testApiHandler configuration.

    Request Patching

    Use requestPatcher to modify the IncomingMessage. This is useful for setting custom headers or properties. Important: When adding or replacing headers, all header names must be lowercase.

    Response Patching

    Use responsePatcher to modify the ServerResponse object before it is passed to your handler.

    Shorthands

    • To set the URL: Use the url property (e.g., url: '/api/path').
    • To set params: Use the params property (e.g., params: { slug: 'my-post' }).
  4. Test Clerk authentication in App Router

    main

    When testing authenticated App Router endpoints, you have two main approaches:

    1. Unit Testing (Mocking): Mock the @clerk/nextjs module using jest.mock to return a specific userId. This is faster and avoids external dependencies.
    2. Integration Testing (Middleware): Instead of mocking, you can manually invoke your Next.js middleware within the appHandler object passed to testApiHandler. This allows you to test the actual interaction between your middleware and the route.

    Important: When using integration tests with middleware, you may need to set a custom url (e.g., ntarh://app/api/authed) if your middleware uses path-based filtering. Also, ensure rejectOnHandlerError: true is set if you want the test to fail on handler errors.

    import { testApiHandler } from 'next-test-api-route-handler';
    import * as appHandler from '../app/api/authed/route';
    import { default as middleware } from '../middleware';
    
    await testApiHandler({
      rejectOnHandlerError: true,
      url: 'ntarh://app/api/authed',
      appHandler: {
        get GET() {
          return async function (...args) {
            const request = args[0];
            const middlewareResponse = await middleware(request, {});
            // ... assertions on middlewareResponse ...
            return await appHandler.GET(...args);
          };
        }
      },
      test: async ({ fetch }) => {
        await expect((await fetch()).json()).resolves.toStrictEqual({
          isAuthed: true,
          userId: 'user_id'
        });
      }
    });
  5. Configure Jest to use the Node environment for API tests

    main

    If your project uses jsdom by default, you may encounter errors (like TypeError: RequestInit: Expected signal...) because jsdom does not support global fetch natively and API routes do not require a DOM.

    To fix this, use a Jest docblock annotation to switch the environment to node for your API test files.

    /**
     * @jest-environment node
     */
    
    import { testApiHandler } from 'next-test-api-route-handler';
    
    test('use the node test environment for all tests in this file', () => {
      //...
    });
  6. Work around global `fetch` mutations in App Router tests

    main

    Next.js's App Router implementation mutates the global fetch function. This can interfere with testing tools like Mock Service Worker (MSW) or Jest spies.

    NTARH sidesteps this by caching the original globalThis.fetch upon import, but it does not restore it after the test returns. If you need to ensure a clean environment, you should manually capture the original fetch and restore it in an afterEach block.

    const originalGlobalFetch = fetch;
    
    afterEach(function () {
      // Undo what Next.js does to the global fetch function
      globalThis.fetch = originalGlobalFetch;
    });
  7. Quick Start: Edge Runtime

    main

    To test an Edge Runtime handler, use the appHandler property. NTARH supports TypeScript generics to provide type safety for the response data. You can use requestPatcher to handle specific Edge requirements like duplex: 'half' for request bodies.

    Note: Only appHandler supports edge functions; the pagesHandler property does not support them.

    import { testApiHandler } from 'next-test-api-route-handler';
    import * as edgeHandler from '../app/your-edge-endpoint/route';
    
    it('does what I want', async function () {
      await testApiHandler<{ success: boolean }>({
        appHandler: edgeHandler,
        requestPatcher(request) {
          return new Request(request, {
            body: dummyReadableStream,
            duplex: 'half'
          });
        },
        async test({ fetch }) {
          await expect((await fetch()).json()).resolves.toStrictEqual({
            success: true
          });
        }
      });
    });
  8. Quick Start: Pages Router

    main

    To test a Pages Router handler, import the handler and its configuration from your pages/api directory and pass it to testApiHandler using the pagesHandler property. You can use requestPatcher to modify the req object.

    import { testApiHandler } from 'next-test-api-route-handler';
    import * as pagesHandler from '../pages/api/your-endpoint';
    
    it('does what I want', async () => {
      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' });
          const { hello } = await res.json();
          expect(hello).toBe('world');
        }
      });
    });
    import { testApiHandler } from 'next-test-api-route-handler';
    import * as pagesHandler from '../pages/api/your-endpoint';
    
    it('does what I want', async () => {
      await testApiHandler<{ hello: string }>({
        pagesHandler,
        test: async ({ fetch }) => {
          const res = await fetch({ method: 'POST', body: 'data' });
          const { hello } = await res.json();
          expect(hello).toBe('world');
        }
      });
    });
  9. Install next-test-api-route-handler

    main

    Install the package as a development dependency using npm:

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

    Note on environments: If you are using jest or vitest with a DOM-like environment (such as jest-environment-jsdom or jsdom), you should not use that environment to test your API handlers. Refer to the project appendix for details on JSDOM support.

    npm install --save-dev next-test-api-route-handler
  10. Ensure `AsyncLocalStorage` is available by importing NTARH first

    main

    The AppRouteRouteModule and its dependencies require AsyncLocalStorage to be available globally and immediately. Since Node.js does not place AsyncLocalStorage in globalThis natively, NTARH injects it.

    To prevent errors, next-test-api-route-handler must be the very first import in your test file.

    import { testApiHandler } from 'next-test-api-route-handler';
    // This must be the first import in the file
  11. Quick Start: App Router

    main

    To test an App Router handler, import the handler from your app directory and pass it to testApiHandler using the appHandler property. You can optionally use requestPatcher to modify the incoming request (e.g., setting headers) and responsePatcher to modify the outgoing response.

    import { testApiHandler } from 'next-test-api-route-handler';
    import * as appHandler from '../app/your-endpoint/route';
    
    it('does what I want', async () => {
      await testApiHandler({
        appHandler,
        requestPatcher(request) {
          request.headers.set('key', process.env.SPECIAL_TOKEN);
        },
        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!' });
        }
      });
    });
    import { testApiHandler } from 'next-test-api-route-handler';
    import * as appHandler from '../app/your-endpoint/route';
    
    it('does what I want', async () => {
      await testApiHandler({
        appHandler,
        async test({ fetch }) {
          const res = await fetch({ method: 'POST', body: 'dummy-data' });
          await expect(res.json()).resolves.toStrictEqual({ hello: 'world!' });
        }
      });
    });
  12. Handle handler exceptions with `rejectOnHandlerError`

    main

    The rejectOnHandlerError boolean controls how exceptions thrown inside your route handler are treated during tests.

    • false (Default): Errors are passed to the Next.js resolver, mimicking production behavior. This may result in a 500 response being returned instead of a test failure.
    • true: The testApiHandler function will reject immediately when an error is thrown in the handler.

    When to use true:

    • When you want to manually handle errors thrown from within the handler.
    • If you are using expect() assertions inside your handler code.
    • If you encounter false negatives where exceptions are thrown but the test does not fail as expected.
    rejectOnHandlerError: true