Skip to main content

json-server vs MSW vs MirageJS: API Mocking for Development 2026

·PkgPulse Team

json-server vs MSW vs MirageJS: API Mocking for Development 2026

TL;DR

Mocking APIs during development and testing is a fundamental developer workflow — working offline, building before the backend is ready, running reproducible tests. Three tools dominate this space for JavaScript developers. json-server is the simplest option — point it at a db.json file, get a full REST API instantly (GET, POST, PUT, PATCH, DELETE) with filtering, pagination, and relationships, no code required; perfect for rapid prototyping. MSW (Mock Service Worker) intercepts real HTTP requests using Service Workers in the browser and Node.js interceptors — your app code makes real fetch calls, MSW intercepts before they leave the process, making it the gold standard for testing with @testing-library/react and Storybook. MirageJS runs a simulated server inside your JavaScript runtime, defines models and routes, and serializes responses — it's more sophisticated than json-server for complex data relationships but more coupled to your app than MSW. For instant mock REST API during prototyping: json-server. For test-safe, request-intercepting mocks that don't change your app code: MSW. For a full fake server with models, relationships, and factories in complex SPAs: MirageJS.

Key Takeaways

  • json-server needs no codejson-server --watch db.json gives you a full REST API
  • MSW intercepts at the network layer — your app code makes real requests, MSW catches them
  • MSW works in both browser and Node — Service Worker in browser, Node interceptor in tests
  • MirageJS runs in-process — no separate server, all mocks live in JavaScript
  • MSW is the testing standard — works seamlessly with Vitest, Jest, Testing Library
  • json-server supports JSON:API — relationships via _embed and _expand query params
  • MirageJS has factoriesserver.createList("post", 10) generates fixture data

Architecture Comparison

json-server       Separate process, real HTTP server on a port
MSW               Network layer interceptor (SW in browser, Node interceptor in tests)
MirageJS          In-process JavaScript fake server, no real HTTP requests

json-server: Zero-Code REST API

json-server turns a JSON file into a complete RESTful API in seconds — no JavaScript required.

Installation and Quick Start

npm install -g json-server
# Or as a dev dependency:
npm install --save-dev json-server
// db.json
{
  "posts": [
    { "id": 1, "title": "Hello World", "authorId": 1, "status": "published" },
    { "id": 2, "title": "Getting Started", "authorId": 2, "status": "draft" }
  ],
  "authors": [
    { "id": 1, "name": "Alice", "email": "alice@example.com" },
    { "id": 2, "name": "Bob", "email": "bob@example.com" }
  ],
  "comments": [
    { "id": 1, "postId": 1, "body": "Great post!" }
  ]
}
json-server --watch db.json --port 3001

Now you have a full REST API:

  • GET /posts → all posts
  • GET /posts/1 → post by ID
  • POST /posts → create post
  • PUT /posts/1 → replace post
  • PATCH /posts/1 → update post
  • DELETE /posts/1 → delete post
  • GET /posts?status=published → filter
  • GET /posts?_page=1&_per_page=10 → pagination
  • GET /posts?_sort=title&_order=asc → sort
  • GET /posts/1?_embed=comments → embed related
  • GET /posts/1?_expand=author → expand relationship

package.json Script

{
  "scripts": {
    "api:mock": "json-server --watch db.json --port 3001 --delay 200"
  }
}

Custom Routes

// routes.json — custom route mapping
{
  "/api/v1/posts": "/posts",
  "/api/v1/posts/:id": "/posts/:id",
  "/api/v1/authors": "/authors",
  "/blog/articles": "/posts"
}
json-server --watch db.json --routes routes.json --port 3001

Custom Middleware (json-server v1.x Programmatic API)

// server.js
const jsonServer = require("json-server");
const server = jsonServer.create();
const router = jsonServer.router("db.json");
const middlewares = jsonServer.defaults();

// Add custom middleware
server.use(middlewares);

// Auth middleware
server.use((req, res, next) => {
  if (req.headers.authorization || req.method === "GET") {
    next();
  } else {
    res.status(401).json({ error: "Unauthorized" });
  }
});

// Custom route
server.get("/api/stats", (req, res) => {
  const db = router.db; // lowdb instance
  const postCount = db.get("posts").size().value();
  res.json({ posts: postCount, timestamp: new Date().toISOString() });
});

server.use(router);
server.listen(3001, () => {
  console.log("Mock API running on http://localhost:3001");
});

MSW: Network Layer Interception

MSW intercepts fetch and XMLHttpRequest calls at the network level — your application code makes real requests, MSW intercepts them before they leave the browser or Node process.

Installation

npm install msw --save-dev
# Initialize Service Worker file:
npx msw init public/ --save

Define Handlers

// src/mocks/handlers.ts
import { http, HttpResponse, delay } from "msw";

interface Post {
  id: number;
  title: string;
  body: string;
  authorId: number;
  status: "draft" | "published";
}

const posts: Post[] = [
  { id: 1, title: "Hello World", body: "First post content.", authorId: 1, status: "published" },
  { id: 2, title: "Getting Started", body: "Second post.", authorId: 2, status: "draft" },
];

export const handlers = [
  // GET /api/posts
  http.get("/api/posts", async ({ request }) => {
    const url = new URL(request.url);
    const status = url.searchParams.get("status");

    await delay(200); // Simulate network latency

    const filtered = status
      ? posts.filter((p) => p.status === status)
      : posts;

    return HttpResponse.json(filtered);
  }),

  // GET /api/posts/:id
  http.get("/api/posts/:id", ({ params }) => {
    const id = Number(params.id);
    const post = posts.find((p) => p.id === id);

    if (!post) {
      return new HttpResponse(null, { status: 404 });
    }

    return HttpResponse.json(post);
  }),

  // POST /api/posts
  http.post<never, Omit<Post, "id">>("/api/posts", async ({ request }) => {
    const data = await request.json();
    const newPost: Post = { ...data, id: posts.length + 1 };
    posts.push(newPost);
    return HttpResponse.json(newPost, { status: 201 });
  }),

  // PATCH /api/posts/:id
  http.patch<{ id: string }, Partial<Post>>("/api/posts/:id", async ({ params, request }) => {
    const id = Number(params.id);
    const updates = await request.json();
    const index = posts.findIndex((p) => p.id === id);

    if (index === -1) {
      return new HttpResponse(null, { status: 404 });
    }

    posts[index] = { ...posts[index], ...updates };
    return HttpResponse.json(posts[index]);
  }),

  // DELETE /api/posts/:id
  http.delete("/api/posts/:id", ({ params }) => {
    const id = Number(params.id);
    const index = posts.findIndex((p) => p.id === id);

    if (index === -1) {
      return new HttpResponse(null, { status: 404 });
    }

    posts.splice(index, 1);
    return new HttpResponse(null, { status: 204 });
  }),
];

Browser Setup (Service Worker)

// src/mocks/browser.ts
import { setupWorker } from "msw/browser";
import { handlers } from "./handlers";

export const worker = setupWorker(...handlers);

// src/main.tsx
async function enableMocking() {
  if (process.env.NODE_ENV !== "development") return;

  const { worker } = await import("./mocks/browser");
  return worker.start({
    onUnhandledRequest: "warn", // Warn when no handler matches
  });
}

enableMocking().then(() => {
  ReactDOM.createRoot(document.getElementById("root")!).render(<App />);
});

Node.js Setup (For Tests)

// src/mocks/node.ts
import { setupServer } from "msw/node";
import { handlers } from "./handlers";

export const server = setupServer(...handlers);

// vitest.setup.ts or jest.setup.ts
import { server } from "./src/mocks/node";

beforeAll(() => server.listen({ onUnhandledRequest: "error" }));
afterEach(() => server.resetHandlers());
afterAll(() => server.close());

Testing with MSW

// src/components/PostList.test.tsx
import { render, screen, waitFor } from "@testing-library/react";
import { http, HttpResponse } from "msw";
import { server } from "../mocks/node";
import { PostList } from "./PostList";

test("renders post list", async () => {
  render(<PostList />);

  // MSW intercepts the GET /api/posts request
  await waitFor(() => {
    expect(screen.getByText("Hello World")).toBeInTheDocument();
    expect(screen.getByText("Getting Started")).toBeInTheDocument();
  });
});

test("handles error state", async () => {
  // Override handler for this test only
  server.use(
    http.get("/api/posts", () => {
      return new HttpResponse(null, { status: 500 });
    })
  );

  render(<PostList />);

  await waitFor(() => {
    expect(screen.getByText("Failed to load posts")).toBeInTheDocument();
  });
});

test("shows empty state", async () => {
  server.use(
    http.get("/api/posts", () => {
      return HttpResponse.json([]);
    })
  );

  render(<PostList />);

  await waitFor(() => {
    expect(screen.getByText("No posts yet")).toBeInTheDocument();
  });
});

MirageJS: In-Process Fake Server

MirageJS creates a fake server inside your JavaScript runtime — it intercepts requests at the XMLHttpRequest/fetch level and responds from an in-memory database with models and serializers.

Installation

npm install miragejs --save-dev

Server Setup

// src/mocks/server.ts
import { createServer, Model, Factory, hasMany, belongsTo } from "miragejs";

export function makeServer({ environment = "development" } = {}) {
  return createServer({
    environment,

    models: {
      post: Model.extend({
        author: belongsTo(),
        comments: hasMany(),
      }),
      author: Model.extend({
        posts: hasMany(),
      }),
      comment: Model.extend({
        post: belongsTo(),
      }),
    },

    factories: {
      post: Factory.extend({
        title(i: number) {
          return `Post ${i + 1}`;
        },
        body() {
          return "Lorem ipsum dolor sit amet...";
        },
        status() {
          return Math.random() > 0.5 ? "published" : "draft";
        },
        createdAt() {
          return new Date().toISOString();
        },
      }),

      author: Factory.extend({
        name(i: number) {
          return `Author ${i + 1}`;
        },
        email(i: number) {
          return `author${i + 1}@example.com`;
        },
      }),
    },

    seeds(server) {
      // Create 3 authors
      const authors = server.createList("author", 3);

      // Create 10 posts with relationships
      authors.forEach((author) => {
        server.createList("post", 3, { author });
      });
    },

    routes() {
      this.namespace = "api";

      // GET /api/posts
      this.get("/posts", (schema, request) => {
        const { status } = request.queryParams;

        if (status) {
          return schema.where("post", { status });
        }
        return schema.all("post");
      });

      // GET /api/posts/:id
      this.get("/posts/:id", (schema, request) => {
        return schema.find("post", request.params.id);
      });

      // POST /api/posts
      this.post("/posts", (schema, request) => {
        const attrs = JSON.parse(request.requestBody);
        return schema.create("post", attrs);
      });

      // PATCH /api/posts/:id
      this.patch("/posts/:id", (schema, request) => {
        const attrs = JSON.parse(request.requestBody);
        const post = schema.find("post", request.params.id);
        return post.update(attrs);
      });

      // DELETE /api/posts/:id
      this.delete("/posts/:id", (schema, request) => {
        const post = schema.find("post", request.params.id);
        return post.destroy();
      });

      // Passthrough for everything else (e.g., auth endpoints you don't mock)
      this.passthrough("https://auth.example.com/**");

      // Simulated delay
      this.timing = 200;
    },
  });
}

Start in Development

// src/main.tsx
if (process.env.NODE_ENV === "development") {
  const { makeServer } = await import("./mocks/server");
  makeServer();
}

ReactDOM.createRoot(document.getElementById("root")!).render(<App />);

Testing with MirageJS

// src/components/PostList.test.tsx
import { render, screen, waitFor } from "@testing-library/react";
import { makeServer } from "../mocks/server";
import { Server } from "miragejs";
import { PostList } from "./PostList";

let server: Server;

beforeEach(() => {
  server = makeServer({ environment: "test" });
});

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

test("renders posts from server", async () => {
  server.createList("post", 3, { status: "published" });

  render(<PostList />);

  await waitFor(() => {
    expect(screen.getAllByRole("article")).toHaveLength(3);
  });
});

test("handles empty state", async () => {
  // No posts created — empty server
  render(<PostList />);

  await waitFor(() => {
    expect(screen.getByText("No posts yet")).toBeInTheDocument();
  });
});

Feature Comparison

Featurejson-serverMSWMirageJS
Setup complexity✅ Zero (JSON file)Medium (handlers)Medium-High
Needs separate process✅ Yes (HTTP server)❌ No❌ No
Works in tests⚠️ Separate process✅ Node interceptor✅ In-process
Browser interceptionNo✅ Service Worker✅ XMLHttpRequest
TypeScriptLimited✅ Full✅ Full
Data relationships_embed/_expandManual✅ Models
FactoriesNoNo (use @faker-js)✅ Built-in
Real HTTP requests✅ (actual fetch)✅ interceptedNo (shimmed)
Custom responsesVia middleware✅ Full control✅ Full control
REST auto-generation✅ Full CRUDManualShortcuts available
GraphQL supportNographql.queryNo
StorybookExternal mock✅ Works nativelyExternal mock
npm weekly600k2.5M250k
GitHub stars22k15k6k

When to Use Each

Choose json-server if:

  • Rapid prototyping — need a REST API in 5 minutes
  • Non-technical stakeholders need to explore a working API
  • Frontend team working before backend is ready
  • Simple CRUD without complex test assertions
  • Need a real HTTP server for testing with Postman/curl

Choose MSW if:

  • Unit and integration testing with React Testing Library or Vitest
  • Mocking in Storybook stories (MSW Storybook addon)
  • Want to test real network paths without changing app code
  • Need both browser development mocking AND test mocking from same handlers
  • GraphQL mocking with graphql.query and graphql.mutation
  • The testing standard — most projects should use MSW for testing

Choose MirageJS if:

  • Complex SPA with many related models and need database-like querying
  • Factories for generating test fixtures are critical
  • Need to simulate relationships (posts → comments → author)
  • Large application with an established Mirage server already in place
  • Need full CRUD from a single server configuration without writing every handler

Methodology

Data sourced from json-server documentation (github.com/typicode/json-server), MSW documentation (mswjs.io/docs), MirageJS documentation (miragejs.com/docs), npm weekly download statistics as of February 2026, GitHub star counts as of February 2026, and community usage patterns from React Testing Library guides and OSS testing documentation.


Related: MSW vs Nock vs axios-mock-adapter API mocking for Node.js-specific HTTP mocking, or supertest vs fastify inject vs Hono API integration testing for testing real HTTP handlers.

Comments

Stay Updated

Get the latest package insights, npm trends, and tooling tips delivered to your inbox.