JSON Mock Data: json-server, Faker.js, MSW & Nock
Last updated:
JSON mock data lets you build and test applications without a live API — tools like json-server, Faker.js, and Mock Service Worker (MSW) generate realistic JSON fixtures in under 5 minutes. json-server spins up a full REST API from a single JSON file with zero configuration; Faker.js generates over 200 data types including names, emails, UUIDs, and ISO dates using a seeded random engine. This guide covers json-server, Faker.js factory functions, MSW browser mocking, Nock HTTP interception, and snapshot testing strategies. You will find TypeScript examples for each approach.
Whether you are prototyping a UI before the backend is ready, writing integration tests, or generating seed data for a database, the right tool depends on where your code runs: json-server is a standalone server process accessible from any client; MSW intercepts at the Service Worker or Node.js level inside your test suite; Nock patches Node.js http modules for unit tests; Faker.js generates the data all of them serve. Understanding the boundaries between these tools eliminates the most common mistake — using a network-level mock where a data generator suffices.
json-server: REST API from a JSON File in 60 Seconds
json-server reads a JSON file and exposes every top-level key as a REST collection — GET, POST, PUT, PATCH, and DELETE all work out of the box. It takes under 2 seconds to start serving a 50-field JSON file on port 3000 with no configuration beyond the data file itself. This makes it the fastest path from "we need an API" to a working endpoint that any HTTP client can call.
# ── Install ───────────────────────────────────────────────────────
npm install -g json-server
# or as a dev dependency
npm install --save-dev json-server
# ── Create db.json ────────────────────────────────────────────────
# db.json — every top-level key becomes a REST collection
{
"users": [
{ "id": 1, "name": "Alice", "email": "alice@example.com", "role": "admin" },
{ "id": 2, "name": "Bob", "email": "bob@example.com", "role": "viewer" }
],
"posts": [
{ "id": 1, "title": "Hello World", "userId": 1, "published": true },
{ "id": 2, "title": "JSON Mocking", "userId": 2, "published": false }
],
"comments": [
{ "id": 1, "postId": 1, "body": "Great post!", "userId": 2 }
]
}
# ── Start the server ──────────────────────────────────────────────
json-server db.json
# Listening on http://localhost:3000
# ── REST endpoints generated automatically ───────────────────────
# GET /users → array of all users
# GET /users/1 → single user by id
# POST /users → create user (auto-assigns id)
# PUT /users/1 → replace user
# PATCH /users/1 → partial update
# DELETE /users/1 → remove user
# ── Filtering, sorting, pagination ───────────────────────────────
# GET /users?role=admin → filter by field value
# GET /users?_sort=name&_order=asc → sort by field
# GET /users?_page=1&_per_page=10 → paginate (returns { data, first, prev, next, last, pages })
# GET /posts?userId=1 → filter by foreign key
# GET /posts?title_like=JSON → regex filter
# ── Relationships ─────────────────────────────────────────────────
# GET /posts/1/comments → comments where postId=1
# GET /posts?_embed=comments → embed related comments inline
# ── package.json script ───────────────────────────────────────────
# "scripts": {
# "mock": "json-server --watch db.json --port 3001 --delay 300"
# }
# --watch: reload on db.json changes
# --delay: simulate network latency (ms)
# ── Custom routes (routes.json) ───────────────────────────────────
# {
# "/api/*": "/$1", → /api/users → /users
# "/v1/users/:id": "/users/:id"
# }
# json-server db.json --routes routes.json
# ── Programmatic usage (Node.js) ──────────────────────────────────
import jsonServer from "json-server";
import { join } from "path";
const server = jsonServer.create();
const router = jsonServer.router(join(__dirname, "db.json"));
const middlewares = jsonServer.defaults();
server.use(middlewares);
server.use(jsonServer.bodyParser);
// Custom middleware: add auth check
server.use((req, res, next) => {
if (req.headers.authorization !== "Bearer test-token") {
res.status(401).json({ error: "Unauthorized" });
} else {
next();
}
});
server.use(router);
server.listen(3001, () => console.log("Mock server running on port 3001"));The --watch flag reloads the server when db.json changes — useful when the data file is generated by a script. The --delay flag adds artificial latency to simulate slow network conditions, surfacing race conditions in the UI before they appear in production. Use json-server programmatically when you need custom middleware — authentication headers, response transformation, or CORS configuration. Write-operations (POST, PUT, PATCH, DELETE) persist changes to db.json on disk; reset the file from version control between test runs to avoid state leakage.
Faker.js Factory Functions for Typed Mock Data
Faker.js v9 generates over 250 locale-aware data types across 60 locales — names, emails, UUIDs, addresses, credit cards, IP addresses, ISO dates, and more. The key pattern is the factory function: a typed function that accepts a Partial override parameter and returns a fully populated object. Factory functions keep mock data centralised, enforce types at compile time, and make per-test customisation a one-liner.
# ── Install ───────────────────────────────────────────────────────
npm install --save-dev @faker-js/faker
# ── Basic usage ───────────────────────────────────────────────────
import { faker } from "@faker-js/faker";
// Single values
faker.string.uuid() // "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d"
faker.person.fullName() // "Mrs. Dena Schuppe"
faker.internet.email() // "alice.smith@example.net"
faker.internet.url() // "https://admiring-cloud.net"
faker.date.recent().toISOString()// "2025-11-14T08:23:41.000Z"
faker.number.int({ min: 1, max: 100 }) // 42
faker.datatype.boolean() // true
faker.helpers.arrayElement(["admin", "viewer", "editor"]) // "viewer"
// ── TypeScript factory function ───────────────────────────────────
interface User {
id: string;
name: string;
email: string;
role: "admin" | "viewer" | "editor";
age: number;
address: {
street: string;
city: string;
country: string;
};
createdAt: string;
}
// src/test/factories/user.ts
export function makeUser(overrides: Partial<User> = {}): User {
return {
id: faker.string.uuid(),
name: faker.person.fullName(),
email: faker.internet.email(),
role: faker.helpers.arrayElement(["admin", "viewer", "editor"] as const),
age: faker.number.int({ min: 18, max: 80 }),
address: {
street: faker.location.streetAddress(),
city: faker.location.city(),
country: faker.location.country(),
},
createdAt: faker.date.recent({ days: 90 }).toISOString(),
...overrides,
};
}
// ── Usage in tests ────────────────────────────────────────────────
// Generate a single user with defaults
const user = makeUser();
// Override specific fields — TypeScript validates the keys and types
const admin = makeUser({ role: "admin" });
const youngUser = makeUser({ age: 20, name: "Charlie" });
// Generate N users
const users = Array.from({ length: 10 }, () => makeUser());
// Generate related data
interface Post {
id: string;
title: string;
userId: string;
published: boolean;
tags: string[];
createdAt: string;
}
export function makePost(overrides: Partial<Post> = {}): Post {
return {
id: faker.string.uuid(),
title: faker.lorem.sentence({ min: 3, max: 8 }),
userId: faker.string.uuid(), // link to a User id in tests
published: faker.datatype.boolean(),
tags: faker.helpers.arrayElements(
["typescript", "json", "api", "testing", "node"],
{ min: 1, max: 3 }
),
createdAt: faker.date.recent({ days: 30 }).toISOString(),
...overrides,
};
}
// ── Locale-aware data ─────────────────────────────────────────────
import { fakerJA as fakerJapanese } from "@faker-js/faker";
const japaneseUser = {
name: fakerJapanese.person.fullName(), // 山田 太郎
address: fakerJapanese.location.city(), // 東京
};
// ── Generating the db.json for json-server ────────────────────────
import { writeFileSync } from "fs";
import { faker } from "@faker-js/faker";
faker.seed(42); // deterministic output
const users = Array.from({ length: 20 }, (_, i) =>
makeUser({ id: String(i + 1) })
);
const posts = Array.from({ length: 50 }, (_, i) => ({
...makePost({ id: String(i + 1) }),
userId: String(faker.number.int({ min: 1, max: 20 })),
}));
writeFileSync("db.json", JSON.stringify({ users, posts }, null, 2));
// Run: json-server db.jsonThe Partial<T> override pattern is the most important Faker.js pattern for testing — it lets tests express only what matters: makeUser({ role: "admin" }) sets the role and fills the rest with realistic noise. This is far better than hardcoding full user objects in every test, which breaks when the User interface gains a required field. The factory is the single source of truth for the shape of mock data. See our guide on TypeScript JSON types for typing strategies that complement this factory pattern.
Mock Service Worker (MSW) for Browser and Node.js
MSW intercepts HTTP requests at the network layer — in browsers via a registered Service Worker, in Node.js via a custom interceptor that wraps the built-in http/https modules. Because MSW operates below the application layer, it works with any HTTP client (fetch, axios, ky, XMLHttpRequest) without modifying application code. The Service Worker script is never imported in production builds, so there is zero bundle impact.
# ── Install ───────────────────────────────────────────────────────
npm install msw --save-dev
# Generate Service Worker file (place in the public/ directory)
npx msw init public/ --save
# ── Define handlers (src/mocks/handlers.ts) ───────────────────────
import { http, HttpResponse } from "msw";
import { makeUser, makePost } from "../test/factories";
import { faker } from "@faker-js/faker";
faker.seed(42);
export const handlers = [
// GET /api/users → return list of mock users
http.get("/api/users", ({ request }) => {
const url = new URL(request.url);
const page = Number(url.searchParams.get("page") ?? 1);
const pageSize = Number(url.searchParams.get("pageSize") ?? 10);
const users = Array.from({ length: pageSize }, () => makeUser());
return HttpResponse.json({
data: users,
meta: { page, pageSize, total: 100 },
});
}),
// GET /api/users/:id
http.get("/api/users/:id", ({ params }) => {
return HttpResponse.json(makeUser({ id: params.id as string }));
}),
// POST /api/users
http.post("/api/users", async ({ request }) => {
const body = await request.json() as Partial<typeof makeUser>;
const user = makeUser(body as Parameters<typeof makeUser>[0]);
return HttpResponse.json(user, { status: 201 });
}),
// PATCH /api/users/:id
http.patch("/api/users/:id", async ({ params, request }) => {
const body = await request.json() as Partial<ReturnType<typeof makeUser>>;
const updated = makeUser({ id: params.id as string, ...body });
return HttpResponse.json(updated);
}),
// DELETE /api/users/:id
http.delete("/api/users/:id", () => {
return new HttpResponse(null, { status: 204 });
}),
// Simulate network error
http.get("/api/broken", () => {
return HttpResponse.error();
}),
// Simulate 500 error with JSON body
http.get("/api/fail", () => {
return HttpResponse.json(
{ error: "Internal server error", code: "SERVER_ERROR" },
{ status: 500 }
);
}),
];
# ── Browser setup (src/mocks/browser.ts) ─────────────────────────
import { setupWorker } from "msw/browser";
import { handlers } from "./handlers";
export const worker = setupWorker(...handlers);
# ── Start MSW in development (src/main.tsx or src/index.tsx) ─────
async function enableMocking() {
if (process.env.NODE_ENV !== "development") return;
const { worker } = await import("./mocks/browser");
return worker.start({ onUnhandledRequest: "bypass" });
}
enableMocking().then(() => {
// mount your app
});
# ── Node.js / test setup (src/mocks/server.ts) ───────────────────
import { setupServer } from "msw/node";
import { handlers } from "./handlers";
export const server = setupServer(...handlers);
# ── Jest / Vitest test setup file ────────────────────────────────
import { server } from "./mocks/server";
beforeAll(() => server.listen({ onUnhandledRequest: "warn" }));
afterEach(() => server.resetHandlers()); // restore defaults between tests
afterAll(() => server.close());
# ── Override handlers per test ────────────────────────────────────
import { http, HttpResponse } from "msw";
import { server } from "../mocks/server";
test("shows error message on 500", async () => {
server.use(
http.get("/api/users", () =>
HttpResponse.json({ error: "Server error" }, { status: 500 })
)
);
// render component, assert error UI is shown
});The server.resetHandlers() call in afterEach is non-optional — without it, a per-test handler override leaks into subsequent tests and produces confusing failures. The onUnhandledRequest: "warn" option logs unhandled requests without failing tests; switch to "error" in strict test suites to catch missing handler coverage. MSW v2 uses the http and HttpResponse API (replacing the v1 rest API) — the new API is fully typed and supports streaming responses. See our guide on JSON API design for structuring the response envelopes your handlers return.
Nock: HTTP Interception for Node.js Tests
Nock patches Node.js http and https modules at the module level — any outbound HTTP request from within the Node.js process is intercepted before it reaches the network. This makes Nock ideal for unit-testing server-side code that calls external APIs. Unlike MSW, Nock does not require a browser or Service Worker; unlike json-server, Nock requires no running process. Nock verifies that every registered interceptor was actually called, catching missing API calls that MSW does not.
# ── Install ───────────────────────────────────────────────────────
npm install nock --save-dev
# ── Basic intercept ───────────────────────────────────────────────
import nock from "nock";
import { makeUser } from "../test/factories";
// Intercept GET https://api.example.com/users
const mockUsers = [makeUser({ id: "1" }), makeUser({ id: "2" })];
nock("https://api.example.com")
.get("/users")
.reply(200, mockUsers, { "Content-Type": "application/json" });
// Any code that calls fetch/axios/got to https://api.example.com/users
// receives mockUsers as the response body
# ── afterEach cleanup ─────────────────────────────────────────────
afterEach(() => {
nock.cleanAll(); // remove pending interceptors
nock.enableNetConnect(); // re-enable real network after nock.disableNetConnect()
});
# ── Assert all interceptors were called ──────────────────────────
test("fetches all users", async () => {
const scope = nock("https://api.example.com")
.get("/users")
.reply(200, mockUsers);
await fetchAllUsers(); // function under test
expect(scope.isDone()).toBe(true); // throws if the request was not made
// or: nock.isDone() — checks all active interceptors
});
# ── Query parameter matching ──────────────────────────────────────
nock("https://api.example.com")
.get("/users")
.query({ page: "1", pageSize: "10" }) // exact query string match
.reply(200, { data: mockUsers, meta: { page: 1, total: 100 } });
# ── Request body matching (POST) ──────────────────────────────────
nock("https://api.example.com")
.post("/users", { name: "Alice", email: "alice@example.com" })
.reply(201, makeUser({ name: "Alice" }));
# ── Chain multiple interceptors ───────────────────────────────────
nock("https://api.example.com")
.get("/users/1").reply(200, makeUser({ id: "1" }))
.get("/users/2").reply(200, makeUser({ id: "2" }))
.delete("/users/1").reply(204);
# ── Error simulation ──────────────────────────────────────────────
nock("https://api.example.com")
.get("/users")
.replyWithError({ message: "ECONNREFUSED", code: "ECONNREFUSED" });
# ── Simulate timeout ──────────────────────────────────────────────
nock("https://api.example.com")
.get("/slow")
.delay(5000) // delay response by 5 seconds
.reply(200, {});
# ── Disable real network in tests ─────────────────────────────────
// In Jest setup file
beforeAll(() => nock.disableNetConnect());
afterAll(() => nock.enableNetConnect());
// Allow only specific hosts (e.g., localhost for test DB)
nock.disableNetConnect();
nock.enableNetConnect("localhost");
# ── Persist an interceptor across multiple requests ───────────────
nock("https://api.example.com")
.get("/config")
.times(Infinity) // respond to every request, not just the first
.reply(200, { feature_flags: { dark_mode: true } });nock.disableNetConnect() in the test setup file prevents real HTTP requests from leaking through during tests — any unintercepted request throws Nock: No match for request, immediately surfacing missing mock coverage. This is stricter than MSW's onUnhandledRequest: "warn" and is appropriate for server-side unit tests where real network calls are always a mistake. The .times(n) method controls how many requests a single interceptor handles before being removed — .times(Infinity) is useful for configuration endpoints called on every request. Combine Nock with JSON testing strategies for end-to-end test coverage of server-side API clients.
Snapshot Testing JSON Responses
Snapshot testing captures the exact JSON output of an API handler or data transformation on the first run and saves it as a reference. Subsequent test runs compare the output to the reference — any difference (an added field, a renamed key, a changed type) fails the test. This is the lowest-effort way to detect unintentional breaking changes to JSON response shapes.
// ── Basic snapshot test (Jest / Vitest) ──────────────────────────
import { makeUser } from "../factories/user";
import { faker } from "@faker-js/faker";
// CRITICAL: seed before generating snapshot data
faker.seed(42);
test("user factory matches snapshot", () => {
const user = makeUser();
expect(user).toMatchSnapshot();
// First run: saves snapshot to __snapshots__/user.test.ts.snap
// Subsequent runs: fails if user shape changes
});
// ── Snapshot of API response shape ───────────────────────────────
import request from "supertest";
import app from "../app"; // Express / Fastify / Hono app
test("GET /api/users response shape", async () => {
faker.seed(42);
const res = await request(app).get("/api/users");
expect(res.status).toBe(200);
// Replace dynamic fields before snapshotting
const stable = res.body.data.map((user: Record<string, unknown>) => ({
...user,
id: "[uuid]", // replace dynamic UUID
createdAt: "[date]", // replace dynamic timestamp
}));
expect({ data: stable, meta: res.body.meta }).toMatchSnapshot();
});
// ── Inline snapshot — snapshot stored in the test file ────────────
test("makePost structure", () => {
faker.seed(1);
const post = makePost();
expect(post).toMatchInlineSnapshot(`
{
"createdAt": "2025-10-26T14:22:07.000Z",
"id": "b9f2c1d3-...",
"published": true,
"tags": ["json", "api"],
"title": "In dolor est ut.",
"userId": "a1b2c3d4-...",
}
`);
});
// ── Update snapshots after intentional change ─────────────────────
// jest --updateSnapshot or vitest --update-snapshots
// ── Snapshot of full HTTP response (including headers) ────────────
test("response headers match snapshot", async () => {
const res = await request(app).get("/api/users");
expect({
status: res.status,
contentType: res.headers["content-type"],
// do not snapshot all headers — too many dynamic values
}).toMatchSnapshot();
});
// ── Per-field assertion vs snapshot — when to use each ────────────
// Use toMatchSnapshot() when:
// - Response has 10+ fields and you want full regression coverage
// - You want to catch accidental field additions / removals
// - The shape is stable and intentional changes are infrequent
// Use individual expect().toBe() when:
// - The field is critical business logic (price, status, role)
// - You want the test to document the specific expected value
// - The snapshot would be mostly dynamic/replaced valuesSnapshot tests are most valuable for catching shape regressions — they break when a field is renamed or removed, surfacing breaking changes before deployment. They are least valuable for fields with dynamic values (UUIDs, timestamps) — replace those with stable placeholders before snapshotting. The toMatchInlineSnapshot() variant stores the snapshot inside the test file itself, making it visible during code review without opening a separate .snap file. Always run jest --updateSnapshot intentionally and review the diff — snapshot updates are code changes that can mask real regressions if auto-applied. See our guide on JSON Schema validation for complementary schema-based assertions.
Seeded Random Data for Reproducible CI Tests
Random test data makes tests non-deterministic — a test that passes locally may fail on CI because Faker.js generated a different email address that triggered a different code path. Seeding the random number generator before each test run ensures every run produces the same sequence of values, making failures reproducible by replaying the same seed on any machine.
// ── Global seed in Jest setup file (jest.setup.ts) ───────────────
import { faker } from "@faker-js/faker";
// Fixed seed: every test in every run gets the same data
faker.seed(12345);
// ── Per-test seed reset (recommended for isolation) ───────────────
import { faker } from "@faker-js/faker";
beforeEach(() => {
faker.seed(12345); // reset to the same seed before each test
});
// Without reset: tests depend on the order they run (fragile)
// With reset: each test starts from the same generator state (robust)
// ── Log seed on CI for debugging ─────────────────────────────────
const SEED = process.env.TEST_SEED ? Number(process.env.TEST_SEED) : 12345;
faker.seed(SEED);
console.log(`Faker seed: ${SEED}`);
// Reproduce a specific CI run locally:
// TEST_SEED=99999 npx jest
// ── Per-worker seeds for parallel test suites ─────────────────────
// In Vitest (vitest.config.ts)
// Each worker gets a unique seed to avoid cross-worker correlation
// vitest.config.ts
export default {
test: {
setupFiles: ["./src/test/setup.ts"],
pool: "threads",
poolOptions: {
threads: {
singleThread: false,
},
},
},
};
// src/test/setup.ts
import { faker } from "@faker-js/faker";
const workerSeed = (Number(process.env.VITEST_POOL_ID) || 0) * 10000 + 42;
faker.seed(workerSeed);
// ── Faker instance with independent seed ──────────────────────────
// Use when you need two independent generators in one test file
import { Faker, en } from "@faker-js/faker";
const userFaker = new Faker({ locale: [en] });
const postFaker = new Faker({ locale: [en] });
userFaker.seed(100);
postFaker.seed(200);
const user = { name: userFaker.person.fullName() };
const post = { title: postFaker.lorem.sentence() };
// userFaker and postFaker advance independently
// ── Stable test data constants (alternative to seeding) ───────────
// For data that must be exactly known (not just reproducible):
// Define constants directly instead of generating them.
export const TEST_USER = {
id: "test-user-id-001",
name: "Test User",
email: "test@example.com",
role: "admin" as const,
age: 30,
address: { street: "123 Main St", city: "Testville", country: "US" },
createdAt: "2025-01-01T00:00:00.000Z",
};
// Use makeUser(TEST_USER) to get a full User with these exact values
// Use constants for IDs in relational data (post.userId = TEST_USER.id)The choice between a global seed and a per-test seed reset depends on test isolation requirements. A global seed (set once) means each test consumes a different slice of the random sequence depending on how many values preceding tests generated — adding a field to a factory changes all downstream seeds. A per-test reset gives every test the same starting point regardless of what ran before, at the cost of tests no longer covering different random values across tests. For CI stability, per-test reset is the safer default. Store the seed in an environment variable (TEST_SEED) so a specific CI failure can be reproduced locally by setting that variable.
Choosing Between json-server, MSW, and Nock
The right mocking tool is determined by where the code under test runs, what HTTP client it uses, and what you want to verify. json-server, MSW, and Nock solve different problems — using the wrong one adds unnecessary complexity. The decision tree is straightforward once the constraints are clear.
// ── Decision matrix ───────────────────────────────────────────────
// ┌──────────────────┬─────────────────┬───────────────┬──────────┐
// │ Criterion │ json-server │ MSW │ Nock │
// ├──────────────────┼─────────────────┼───────────────┼──────────┤
// │ Runs in │ Separate process│ Browser/Node │ Node only│
// │ HTTP client │ Any │ fetch/XHR/any │ Any* │
// │ Needs port │ Yes (3000) │ No │ No │
// │ Production risk │ None (separate) │ None (SW) │ None │
// │ GraphQL support │ No │ Yes │ No │
// │ Verify calls │ No │ No │ Yes │
// │ Browser mocking │ Yes (cross-net) │ Yes (in-proc) │ No │
// │ Best for │ Prototyping │ Component/E2E │ Unit │
// └──────────────────┴─────────────────┴───────────────┴──────────┘
// * Nock intercepts Node.js http/https — does not intercept undici (Node 18+ fetch)
// unless nock.enableFetchMocking() is called or the fetch polyfill uses http.
// ── When to use json-server ───────────────────────────────────────
// 1. UI team needs a backend before the API is built
// 2. Mobile / native app needs a real HTTP endpoint (no code-level mocking)
// 3. Postman / curl / Swagger UI testing against a mock backend
// 4. E2E tests with Playwright/Cypress where the browser makes real requests
// → json-server runs as a separate process alongside the test runner
// ── When to use MSW ───────────────────────────────────────────────
// 1. React / Vue component tests (Vitest, Jest + Testing Library)
// 2. Storybook integration — mock API calls without a running backend
// 3. E2E tests where request interception at the browser level is needed
// 4. Tests that cover both browser and Node.js (isomorphic fetch clients)
// 5. GraphQL API mocking
// ── When to use Nock ──────────────────────────────────────────────
// 1. Server-side unit tests (Express middleware, API clients, cron jobs)
// 2. Tests that must verify specific HTTP calls were made (nock.isDone())
// 3. Testing retry logic, timeouts, and error responses in Node.js code
// 4. Legacy code using got, axios, or node-fetch (not undici/native fetch)
// ── Combining tools ───────────────────────────────────────────────
// Pattern: json-server for dev, MSW for component tests, Nock for unit tests
// package.json
// {
// "scripts": {
// "dev": "next dev",
// "mock:api": "json-server db.json --port 3001 --watch",
// "dev:mock": "concurrently 'npm run dev' 'npm run mock:api'",
// "test": "vitest", // uses MSW via setupServer
// "test:unit": "jest" // server-side code, uses Nock
// }
// }
// ── Faker.js role in each scenario ───────────────────────────────
// json-server: run a generate-db script (faker.seed(42); writeFileSync("db.json",...))
// MSW handlers: call makeUser() / makePost() inside handler functions
// Nock replies: pass makeUser() directly to .reply(200, makeUser())
// Snapshots: faker.seed(42) before generateing snapshot data
// ── Migrating from json-server to MSW ────────────────────────────
// 1. Create MSW handlers that mirror each json-server route
// 2. Move db.json data into factory functions (makeUser, makePost)
// 3. Replace json-server start with server.listen() in test setup
// 4. Remove json-server from CI startup scripts
// → Result: faster tests (no process spawn), better TypeScript typesThe most common mistake is using json-server for component tests — it requires a running process, introduces network latency, and cannot be controlled per-test (different responses for different test cases require separate server instances or complex state management). MSW solves all three problems for in-process tests. Conversely, using MSW for server-side Node.js code is complicated by MSW's Service Worker architecture — Nock is simpler and more appropriate for pure Node.js unit tests. For a complete picture of how these tools fit into a testing pipeline, see our guide on JSON testing strategies.
Key Terms
- json-server
- A Node.js CLI tool (
npm install -g json-server) that reads adb.jsonfile and exposes every top-level key as a full REST collection with GET, POST, PUT, PATCH, and DELETE endpoints. It starts in under 2 seconds, supports filtering (?role=admin), sorting (?_sort=name), pagination (?_page=1&_per_page=10), and relationship traversal (/users/1/posts). Write-operations persist changes todb.jsonon disk. json-server is best for rapid UI prototyping and E2E testing against a real HTTP endpoint. It requires a running process and an open port, which makes it unsuitable for in-process component tests. - Mock Service Worker (MSW)
- An API mocking library that intercepts HTTP requests at the network layer using a browser Service Worker (
msw/browser) or a Node.js interceptor (msw/node). Handlers are defined usinghttp.get(),http.post(), andHttpResponse.json()from themswpackage. MSW works with any HTTP client — fetch, XMLHttpRequest, axios, ky — without modifying application code. The Service Worker file (mockServiceWorker.js) is never imported in production. MSW v2 introduced thehttpandHttpResponseAPI, replacing the v1restAPI. It also supports GraphQL mocking viagraphql.query()andgraphql.mutation(). - Nock
- A Node.js HTTP mocking and interception library that patches the built-in
httpandhttpsmodules. Intercepts are defined by chainingnock(baseUrl).get(path).reply(status, body). Nock verifies that every registered interceptor was called viascope.isDone()ornock.isDone()— this distinguishes it from MSW and json-server, which do not assert that mocked endpoints were actually requested. Nock supports request body matching, query parameter matching, response delays (.delay(ms)), repeated replies (.times(n)), and error simulation (.replyWithError()). It does not intercept requests made through Node.js 18's nativefetch(undici) by default; for that, use MSW's Node.js server or enable Nock's fetch mocking explicitly. - Faker.js
- An open-source JavaScript library (
npm install @faker-js/faker) for generating realistic fake data. Faker.js v9 provides 250+ data generators across categories includingperson(names, prefixes),internet(emails, URLs, IPs),location(addresses, cities, countries),date(past, future, recent, between),number,string(uuid, alphanumeric),lorem(words, sentences, paragraphs), andhelpers(arrayElement, shuffle, maybe). Locale-aware data is available through named exports (fakerJA,fakerDE, etc.) or theFakerconstructor with a locale array. The seed value (faker.seed(n)) initialises the pseudo-random engine for deterministic output. - factory function
- A function that creates and returns a mock data object with sensible defaults and an optional
Partial<T>override parameter. The pattern is:function makeUser(overrides: Partial<User> = { }): User { return { id: faker.string.uuid(), ...overrides }; }. The override spread at the end allows per-test customisation without repeating all fields. Factory functions centralise mock data shapes — when theUserinterface gains a required field, only the factory needs updating. They produce typed output, so TypeScript catches mismatches between the factory and the interface. Export factory functions from a dedicatedsrc/test/factories/directory so every test file imports from the same source. - snapshot testing
- A testing technique where the output of a function is serialised and saved to a
.snapfile on the first run. Subsequent runs compare the output to the saved snapshot and fail if any difference is detected. In Jest and Vitest, useexpect(value).toMatchSnapshot()(saves to a separate__snapshots__file) orexpect(value).toMatchInlineSnapshot()(stores the snapshot inline in the test file). Snapshot tests are most useful for detecting unintentional changes to JSON response shapes. Dynamic values (UUIDs, timestamps) must be replaced with stable placeholders before snapshotting to prevent snapshots from becoming stale on every run. Update snapshots intentionally withjest --updateSnapshotand always review the diff. - seed value
- An integer that initialises a pseudo-random number generator (PRNG) to a known state. Given the same seed, the PRNG produces the same sequence of values on every call, on every machine, and in every language runtime that uses the same algorithm. In Faker.js, call
faker.seed(42)before generating data — every subsequentfaker.*call returns the same value as any other run with the same seed and the same call sequence. Seeds are essential for deterministic CI tests: store the seed in an environment variable, log it on CI, and use it to reproduce failures locally. For parallel test suites, use a unique seed per worker (workerIndex * 10000 + fixedBase) to prevent cross-worker value collisions.
FAQ
What is the difference between json-server and MSW for mocking JSON APIs?
json-server runs as a standalone HTTP server process on a real port — any client (browser, Postman, curl, mobile app) can reach it over the network. It is best for UI prototyping and E2E tests where a real server is needed. MSW intercepts requests inside the browser (via Service Worker) or inside the Node.js test process (via http module patching) — no separate server or port is required. MSW is best for component tests and integration tests because it runs inside the test process, supports per-test handler overrides, and produces zero production bundle impact. json-server requires a startup script and port management; MSW requires importing a setup file in tests. Both support REST; MSW also supports GraphQL. For Playwright or Cypress E2E tests where the browser makes real network requests, json-server is simpler; for Vitest or Jest component tests, MSW is the right choice.
How do I generate realistic fake JSON data with Faker.js?
Install Faker.js with npm install --save-dev @faker-js/faker. Import and call generators: import { faker } from "@faker-js/faker". Common generators: faker.string.uuid() for UUIDs, faker.person.fullName() for names, faker.internet.email() for emails, faker.date.recent().toISOString() for ISO timestamps, faker.number.int({ min: 1, max: 100 }) for integers, faker.helpers.arrayElement(["admin", "viewer"]) for enum values. Wrap calls in a typed factory function: function makeUser(overrides: Partial<User> = { }): User { return { id: faker.string.uuid(), name: faker.person.fullName(), ...overrides }; }. Set a seed for deterministic output: faker.seed(42) — the same sequence of values is generated on every run. Generate N records with Array.from({ length: 10 }, makeUser).
How do I use MSW to mock a REST API in a React application?
Install MSW (npm install msw --save-dev) and generate the worker file (npx msw init public/ --save). Define handlers in src/mocks/handlers.ts: import { http, HttpResponse } from "msw"; export const handlers = [http.get("/api/users", () => HttpResponse.json([makeUser()]))]. For development, create src/mocks/browser.ts: import { setupWorker } from "msw/browser"; export const worker = setupWorker(...handlers), then call worker.start() before mounting the app. For tests, create src/mocks/server.ts: import { setupServer } from "msw/node"; export const server = setupServer(...handlers). In the test setup file: beforeAll(() => server.listen()); afterEach(() => server.resetHandlers()); afterAll(() => server.close()). Override handlers per test with server.use(http.get("/api/users", () => HttpResponse.json([], { status: 500 })).
How do I intercept HTTP requests with Nock in Jest tests?
Install Nock (npm install nock --save-dev). Before the code under test runs, register an interceptor: import nock from "nock"; nock("https://api.example.com").get("/users").reply(200, [makeUser()]). Any Node.js http or https request to that URL is intercepted and the mock body is returned. Clean up after each test with afterEach(() => nock.cleanAll()). Assert that the interceptor was called: const scope = nock(...).get(...).reply(...); await callApi(); expect(scope.isDone()).toBe(true). Match query parameters: .query({ page: "1" }). Match request bodies: .post("/users", { name: "Alice" }). Simulate errors: .replyWithError('ECONNREFUSED'). Simulate timeouts: .delay(5000).reply(200, { }). Disable real network in all tests: beforeAll(() => nock.disableNetConnect()).
How do I create a factory function that returns typed mock JSON in TypeScript?
Define a TypeScript interface that matches your API response shape. Write a factory function that returns the interface type and accepts a Partial<T> override parameter: interface User { id: string; name: string; role: "admin" | "viewer"; } then function makeUser(overrides: Partial<User> = { }): User { return { id: faker.string.uuid(), name: faker.person.fullName(), role: "viewer", ...overrides }; }. The spread of overrides at the end lets tests set only the fields that matter: makeUser({ role: "admin" }). TypeScript validates override keys against the User interface and ensures value types match — makeUser({ role: "superuser" }) is a compile error. Export the factory from src/test/factories/user.ts and import it in every test that needs a user. Add a makeUsers(n: number) helper that calls Array.from({ length: n }, makeUser).
How do I seed Faker.js for deterministic test data?
Call faker.seed(n) with any integer before generating data. Every subsequent faker.* call returns the same value as any other run that started with the same seed and the same call sequence: faker.seed(42); faker.person.fullName() always returns the same name. For test isolation, reset the seed in a beforeEach block so each test starts from the same generator state regardless of what previous tests generated. Store the seed in an environment variable (TEST_SEED) and log it at the start of each CI run — when a test fails, set TEST_SEED to the logged value locally to reproduce exactly. For parallel test workers, use a per-worker seed: faker.seed(workerIndex * 10000 + 42). To create independent generator instances, use the Faker constructor: const f = new Faker({ locale: [en], seed: 99 }).
What is snapshot testing and how does it work with JSON responses?
Snapshot testing captures the serialised output of a value on the first run and saves it to a .snap file. On every subsequent run, the output is compared to the saved snapshot — any difference fails the test. Use expect(value).toMatchSnapshot() in Jest or Vitest. The first run always passes (it creates the snapshot); the second run fails if the value changed. For JSON API responses, snapshot the response body after replacing dynamic fields: const stable = { ...body, id: "[uuid]", createdAt: "[date]" }; expect(stable).toMatchSnapshot(). Always seed Faker.js before generating snapshot data so the generated values are stable across runs. Update snapshots intentionally with jest --updateSnapshot and review the diff in your pull request to ensure the change was intentional. Use inline snapshots (toMatchInlineSnapshot()) for small objects so the expected value is visible without opening a separate .snap file.
How do I mock paginated JSON API responses?
Build a page envelope factory: function makePage<T>(items: T[], page = 1, pageSize = 10, total = 100) { return { data: items, meta: { page, pageSize, total, totalPages: Math.ceil(total / pageSize) } }; }. With MSW, parse query parameters inside the handler: http.get("/api/users", ({ request }) => { const url = new URL(request.url); const page = Number(url.searchParams.get("page") ?? 1); return HttpResponse.json(makePage(Array.from({ length: 10 }, makeUser), page)); }. With Nock, register one interceptor per page: nock(base).get("/users").query({ page: "1" }).reply(200, makePage(users1)); nock(base).get("/users").query({ page: "2" }).reply(200, makePage(users2, 2)). With json-server, pagination is built in — ?_page=1&_per_page=10 returns { data, first, prev, next, last, pages } automatically. Test that your client fetches all pages by asserting the total count equals meta.total after the last page.
Further reading and primary sources
- json-server on GitHub — Official json-server repository — full REST API from a JSON file with zero configuration
- Faker.js documentation — Official Faker.js docs — 250+ locale-aware data generators with TypeScript support
- MSW documentation — Official Mock Service Worker docs — browser and Node.js API mocking via Service Worker
- Nock on GitHub — Official Nock repository — HTTP server mocking and expectations for Node.js
- Jest snapshot testing — Official Jest docs on snapshot testing — capturing and diffing serialised output