Skip to main content

json-server vs MSW vs MirageJS 2026

·PkgPulse Team
0

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

Ecosystem & Community

MSW is the dominant force in modern API mocking. Created by Artem Zakharchenko, it reached 2.5 million weekly downloads by early 2026 — a testament to its adoption in the React Testing Library community. The MSW Storybook addon (msw-storybook-addon) is used by hundreds of thousands of projects to mock API calls inside component stories. MSW v2 (released in 2023) was a breaking-change rewrite that modernized the API from the older rest.get() syntax to http.get() and HttpResponse, and the community completed the migration relatively smoothly. For the test frameworks that run alongside MSW, see best JavaScript testing frameworks 2026.

json-server (22k GitHub stars) remains popular despite being maintained by a single developer. Its strength is simplicity — you can explain the entire concept in one sentence, which makes it accessible to junior developers and useful in onboarding documentation. The v1 rewrite moved json-server from lowdb 1.x to a more modern architecture, but the core db.json → REST API workflow remains unchanged.

MirageJS has a smaller and more stable community. With 6k GitHub stars and 250k weekly downloads, it's the choice for teams already invested in its model/factory system. The EmberJS community originally built Mirage, and many Ember-to-React migrations retain Mirage for its data modeling capabilities that MSW doesn't provide out of the box.

For teams building with Next.js and React Query or SWR, the combination of MSW for test mocking and json-server for rapid prototyping covers the vast majority of API mocking needs without heavy configuration.

Real-World Adoption

MSW is the official recommended mocking solution for React Testing Library, which is itself the recommended testing library for React. Kent C. Dodds, the creator of Testing Library, actively advocates for MSW as the correct architectural approach — intercepting at the network level rather than mocking specific modules ensures tests are testing the real application behavior.

The Storybook ecosystem has heavily adopted MSW. The msw-storybook-addon allows stories to use the same handler definitions as tests, meaning a single handlers.ts file can serve both development mocking (via the browser Service Worker) and Storybook stories. Teams report this reduces the "works in Storybook but not in tests" class of bugs significantly.

json-server sees significant adoption in workshops, tutorials, and bootcamp projects precisely because it requires no programming knowledge to set up. Tools like Create React App tutorials and countless YouTube courses use json-server as the backend for frontend exercises. This educational use case keeps its download numbers healthy even as production use shifts toward MSW. For API mocking that needs best-in-class Node.js testing support, see best nodejs API testing and mocking libraries 2026.

Large enterprise React codebases commonly have all three tools simultaneously: json-server for rapid API exploration, MSW for unit and integration tests, and MirageJS in legacy parts of the codebase that haven't been migrated.

Performance & Developer Experience

json-server adds measurable overhead to the development workflow because it runs as a separate process. Every request from your app crosses a real network boundary to localhost:3001, which adds 1-5ms per request but more importantly means you need to start two processes instead of one. Teams typically add json-server to their package.json start script alongside the dev server (concurrently "next dev" "json-server --watch db.json").

MSW has near-zero performance overhead in tests because the interception happens in-process. In browser development mode, the Service Worker adds a registration step on first load, but subsequent requests are intercepted with minimal latency. The trade-off is an extra file (public/mockServiceWorker.js) that needs to be committed to your repository and kept up to date when MSW is upgraded.

MirageJS's performance depends on the complexity of your server definition. For simple CRUD, it's fast. For complex models with many relationships and serializers, the in-process JavaScript computation can slow test suites. Teams with 500+ tests using MirageJS sometimes see total test suite times of 10-15 minutes, while equivalent MSW setups run in 2-3 minutes.

From a developer experience standpoint, MSW's approach of "write real request handlers, get type safety via TypeScript generics" is the most maintainable. When your API changes, you update the handler types and TypeScript guides you to all the places that need updating.

Migration Guide

Migrating from MirageJS to MSW is the most common migration teams undertake in 2026. The key insight is that MSW doesn't have factories or data models — you use @faker-js/faker to generate realistic data in your handlers. The migration pattern is: replace makeServer() calls in test setup files with server.listen(), replace per-test server.createList() calls with server.use(http.get(...)) overrides that return the specific data needed for each test, and remove the Mirage server definition entirely over time.

The biggest migration challenge is tests that relied on Mirage's model relationships — schema.find("post", id).author is more convenient than manually constructing the response object in an MSW handler. The payoff is faster tests, simpler setup, and the ability to share handlers between browser dev mode and test mode.

Migrating from json-server to MSW for testing is straightforward. The json-server setup remains for prototyping while MSW handles tests. They serve different purposes and can coexist indefinitely.

Final Verdict 2026

For new projects in 2026, the clear recommendation is MSW as the primary mocking tool. Its network-level interception model is architecturally correct, its TypeScript support is excellent, and its adoption in the React ecosystem means you'll find answers to any question quickly. Start with MSW for both browser development mocking and test mocking — sharing handlers across contexts is one of its strongest features.

Add json-server when you need a real HTTP server for quick API exploration, Postman testing, or stakeholder demos. It takes 30 seconds to set up and serves its purpose well.

Use MirageJS only if you're maintaining an existing codebase that already uses it heavily. For new code, MSW is the better choice even when you need complex data relationships — the slight added verbosity is worth the architectural benefits.

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: Best JavaScript Testing Frameworks 2026 for a broader testing landscape overview, node:test vs Vitest vs Jest for test runner comparisons, and Best React Component Libraries 2026 for the component ecosystem context.

The 2026 JavaScript Stack Cheatsheet

One PDF: the best package for every category (ORMs, bundlers, auth, testing, state management). Used by 500+ devs. Free, updated monthly.