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 mockingpugNo 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
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:
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:
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.
Recipe C: cookie-based live toggle, no rebuild
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:
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:
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.