mockingpug
Guides

Next.js

A real App Router Route Handler, backed by the same query resolver

Quick start for mocking a REST API in a Next.js App Router project: one catch-all Route Handler, no separate server process, real filesystem access so mock/ is read the same way the CLI reads it.

1. Install

npm install mockingpug

No msw needed here: a Next.js Route Handler already runs inside a real server, so there's nothing to intercept, only requests to answer directly.

2. Describe your data

Same mock/ convention as the CLI (and mockingpug/react). See Getting Started.

3. mock.config.js at your project root

module.exports = {
  dir: 'mock',
  seed: 'my-app',
  baseUrl: '/api',
  persist: { adapter: 'file', strategy: 'always' },
};

If you omit this file entirely, sane defaults are used (dir: 'mock', baseUrl: '/api', file-backed persistence). See Reference → mock.config.js for the full option reference, including limits (per-entity amount/array caps, enforced by doctor) and runtime (delay/errorRate synthetic latency and failures, applied to every request this Route Handler answers, same as mockingpug/react). Editing mock.config.js and restarting (or waiting for the file watcher, see below) changes runtime values too, but see Devtools → Next.js for a live toggle without a restart.

Per-entity bypass (either the schema-level flag or the runtime bypass()/unbypass() calls documented in the React guide) is React/MSW-specific and has no effect here: a Next.js Route Handler is the real server endpoint, so there's no upstream request to "pass through" to. Use the rewrites() recipe below instead to route a specific real backend path around the mock entirely.

4. The catch-all Route Handler

app/api/[[...mock]]/route.ts
import { createNextHandlers, getMockContext, type NextRouteContext } from 'mockingpug/next';

const handlersPromise = getMockContext(process.cwd()).then(({ ctx }) => createNextHandlers(ctx));

export const GET = async (request: Request, routeCtx: NextRouteContext) => (await handlersPromise).GET(request, routeCtx);
export const POST = async (request: Request, routeCtx: NextRouteContext) => (await handlersPromise).POST(request, routeCtx);
export const PUT = async (request: Request, routeCtx: NextRouteContext) => (await handlersPromise).PUT(request, routeCtx);
export const PATCH = async (request: Request, routeCtx: NextRouteContext) => (await handlersPromise).PATCH(request, routeCtx);
export const DELETE = async (request: Request, routeCtx: NextRouteContext) => (await handlersPromise).DELETE(request, routeCtx);

getMockContext() loads mock.config.js, parses every schema (the same loader the CLI uses), and reconciles/generates the store, so the very first request already has data, without running mpug generate as a separate step. It's memoized per projectDir for the life of the process, so next dev doesn't re-scan mock/ and re-run reconciliation on every request.

app/api/[[...mock]]/route.ts (double brackets: an optional catch-all) means /api itself, /api/user, and /api/user/1 all route here; routeCtx.params.mock is undefined/['user']/['user', '1'] respectively. NextRouteContext's params type accepts both the pre-15 plain-object shape and 15+'s Promise-wrapped one, so the same handler code works either way.

Switching mock ↔ real API

mockingpug doesn't try to resolve where your real backend lives, that stays your app's own configuration (env vars, etc). The standard recipe is Next's own rewrites(), which natively supports an absolute destination:

next.config.js
module.exports = {
  async rewrites() {
    if (process.env.MOCK_MODE !== 'mock') {
      return [{ source: '/api/:path*', destination: `${process.env.REAL_API_URL}/:path*` }];
    }
    return []; // /api/** stays on the catch-all Route Handler above
  },
};

For defense-in-depth (in case rewrites() isn't wired up everywhere, or the route somehow ships in a build where it shouldn't), guard the handlers themselves:

export const GET = async (request: Request, routeCtx: NextRouteContext) => {
  if (process.env.MOCK_MODE !== 'mock') {
    return new Response('Not Found', { status: 404 });
  }
  return (await handlersPromise).GET(request, routeCtx);
};

mockingpug does not exclude this route from your production build for you: that's your bundler/deploy config's job (e.g. not shipping the file, or an environment check like above). See Security for the full checklist.

Recipe B: one Route Handler, decided at request time

rewrites() above is a build-time decision: which environment's build this is decides mock vs. real, once, for the whole deployment. If you'd rather keep a single deployed Route Handler and decide per request instead (no separate build/rewrite config to keep in sync), createProxyHandler() wraps createNextHandlers() with an opt-in forwarding proxy:

app/api/[[...mock]]/route.ts
import { createProxyHandler, getMockContext, type NextRouteContext } from 'mockingpug/next';

const handlersPromise = getMockContext(process.cwd()).then(({ ctx }) =>
  createProxyHandler({ ctx, target: process.env.REAL_API_URL! }),
);

export const GET = async (request: Request, routeCtx: NextRouteContext) => (await handlersPromise).GET(request, routeCtx);
export const POST = async (request: Request, routeCtx: NextRouteContext) => (await handlersPromise).POST(request, routeCtx);
export const PUT = async (request: Request, routeCtx: NextRouteContext) => (await handlersPromise).PUT(request, routeCtx);
export const PATCH = async (request: Request, routeCtx: NextRouteContext) => (await handlersPromise).PATCH(request, routeCtx);
export const DELETE = async (request: Request, routeCtx: NextRouteContext) => (await handlersPromise).DELETE(request, routeCtx);

For every request, createProxyHandler() calls shouldMock(request) (defaults to process.env.MOCK_MODE === 'mock', the same env var the rewrites() recipe uses) — true answers from the mock exactly like createNextHandlers() would, false forwards the method, query string, headers (minus hop-by-hop ones like host/connection), and body to target, and returns the response as-is.

This is a documented convenience, not a transparent API gateway replacement: it adds an extra hop through the Next.js server, buffers the whole request/response body (no streaming), and does not attempt anything beyond a plain pass-through. Verify authorization headers, cookies, and large payloads behave the way you expect against your specific backend before relying on it in production; if you need streaming or fine-grained control over what gets forwarded, write your own forwarding logic instead — this exists to cover the common case cheaply, not every case.

Recipes A and B both decide mock-vs-real for an entire build/deploy. createLiveToggleMiddleware() adds a runtime, per-request override on top of either one, driven by a cookie, useful for QA poking at a prod-like backend from a currently-deployed preview build without touching env vars or redeploying:

middleware.ts
import { createLiveToggleMiddleware } from 'mockingpug/next';

export const middleware = createLiveToggleMiddleware({ target: process.env.REAL_API_URL! });
export const config = { matcher: '/api/:path*' };

This runs in middleware.ts, not the Route Handler: a rewrite has to happen before routing decides anything, and middleware is the one place in the Next.js request lifecycle where that's possible. config.matcher is still your own responsibility, same as any Next.js middleware.

By default it reads a mockingpug-live cookie; a value of "real" on a request under baseUrl (defaults to /api, matching the transport) rewrites it to target, preserving the sub-path and query string. Anything else (including the cookie being absent) falls through to the mock Route Handler untouched. Flip the cookie from client-side code with the matching helper from mockingpug/next/client:

import { setLiveToggleCookie, getLiveToggleCookie } from 'mockingpug/next/client';

<button onClick={() => setLiveToggleCookie(!getLiveToggleCookie())}>
  Toggle real network
</button>

It's a plain first-party, same-origin cookie: no server round-trip to flip it, middleware.ts picks it up on the very next request. This is not wired into <MockDevtools>'s panel: it's a separate, opt-in recipe you assemble yourself with your own toggle UI, since (unlike mockingpug/react's mock/off switch) it needs its own middleware.ts file to actually work — see Devtools → Next.js for what the panel does provide out of the box.

Recipe D: branch per call site instead of routing through /api/*

Recipes A-C all assume your app calls a single mock base path (/api/*) and something decides mock-vs-real for that path as a whole. If you already have a real API client and would rather no production request ever pass anywhere near a Route Handler that could serve a mock, branch in your own data-fetching layer instead, with a build-time flag deciding which function runs:

shared/api.ts
const useMocks = process.env.USE_MOCKS !== 'false' && process.env.NODE_ENV !== 'production';

export async function getUsers() {
  if (useMocks) {
    const res = await fetch(`${process.env.NEXT_PUBLIC_APP_URL}/api/user`);
    return (await res.json()).data;
  }
  return fetchJson('/users'); // your real backend client
}

There's no HTTP-level routing decision at all here (no rewrites(), no proxy, no middleware): useMocks is a plain constant computed once at process start from process.env, and every call site branches on it directly. This is the simplest option when you already have a real API client per endpoint and just want to swap in mock data for development/CI. It's a per-call-site decision, not a per-request one, though — the cookie-based toggle from Recipe C (and <MockDevtools>'s panel in general) has no effect on this pattern, since nothing here is listening for a cookie. Toggle it with the env var and restart, the same way you'd flip MOCK_MODE in Recipe A.

Live schema reloading

getMockContext() watches your resolved mock dir and mock.config.js (node:fs's recursive watch()). Editing a schema while next dev is running invalidates the cached context automatically, so the next request picks it up without a manual restart. This is best-effort: some platforms/filesystems don't support recursive watching (older Linux kernels, some network filesystems); if that's the case here, watching is silently skipped and you're back to restarting the dev server, but nothing crashes either way.

Known limitations

No serverless-specific handling. Every mutating request (POST/PUT/PATCH/DELETE) with persist.adapter: 'file' writes synchronously to .mockingpug/db. On a platform without a persistent filesystem between invocations (most serverless hosts), those writes won't survive the next cold start. Use persist.adapter: 'memory' there if persistence across requests isn't required, or accept that 'file' behaves like 'fresh' in practice on such platforms.

Logs and errors

Everything mockingpug logs is prefixed [mockingpug]. Run npx mpug doctor to validate the exact same schemas the Route Handler loads, without starting next dev first, useful in CI (doctor --strict turns warnings like orphaned entities into a hard failure). At request time, an unexpected internal failure always responds with a generic 500 body ({ "error": { "source": "mockingpug", "message": "internal error" } }): the full error, including stack trace, only goes to your server-side console, never to the client.

On this page