Mocking Complex APIs with Mirage JS

Build realistic, fully-functional mock APIs directly in the browser for faster development and comprehensive testing.

What is Mirage JS and Why It Matters

Mirage JS is an API mocking library that runs entirely in the browser, allowing developers to build, test, and share JavaScript applications without depending on a backend server. Unlike simple mock functions or external mock servers, Mirage provides a full-featured mock server with a database, routing system, serializers, and factories for generating realistic test data.

The library intercepts fetch and XMLHttpRequest calls, meaning your application code makes normal API calls but receives responses from Mirage instead of a remote server. This approach offers several advantages over alternative mocking strategies.

For teams practicing test-driven development or building applications before backend services are available, Mirage JS eliminates the bottlenecks that typically slow down frontend development. The ability to create self-contained, working applications without external dependencies also proves invaluable for creating demos, prototyping ideas, and testing edge cases that would be difficult or expensive to reproduce with real services.

Key Benefits of Using Mirage JS

No External Dependencies

Mock APIs run entirely in the browser without needing separate servers or services.

Transparent Integration

Your application uses normal API interfaces, switching seamlessly between mock and real endpoints.

Realistic Test Data

Factories and seeds generate varied, realistic data for comprehensive testing scenarios.

Relationship Support

Mock complex data structures with belongsTo, hasMany, and other relationship types.

Setting Up Mirage JS in Your Project

Getting started with Mirage JS requires installing the library and creating a server definition that defines your mock API's behavior. The setup process varies slightly depending on your framework, but the core concepts remain consistent across different environments.

First, install Mirage JS using your preferred package manager:

npm install miragejs
# or
yarn add miragejs

After installation, create a file to define your Mirage server that configures routes, models, and seed data. The server should be initialized early in your application's lifecycle, often in the main entry point or a dedicated setup file. This approach integrates seamlessly with modern web development workflows that emphasize rapid iteration and comprehensive testing.

Basic Mirage Server Setup
1import { createServer, Model } from "miragejs";2 3export function makeServer({ environment = "development" } = {}) {4 return createServer({5 environment,6 models: {7 user: Model,8 post: Model,9 },10 routes() {11 this.namespace = "api";12 this.timing = 750;13 14 this.get("/users", (schema) => {15 return schema.users.all();16 });17 18 this.get("/users/:id", (schema, request) => {19 let id = request.params.id;20 return schema.users.find(id);21 });22 },23 });24}

Defining Routes and Request Handlers

Mirage's routing system mirrors the structure of RESTful APIs, with methods corresponding to HTTP verbs. You define routes using this.get(), this.post(), this.put(), this.patch(), and this.delete() within the routes function. Each handler receives the request and can access the database schema to return appropriate responses.

Route handlers can access request data through the request object, including URL parameters, query strings, request headers, and request bodies. This enables you to implement realistic API behavior, including filtering, pagination, and validation.

Route Handler with Pagination
1this.get("/posts", (schema, request) => {2 let queryParams = new URL(request.url, "http://localhost").searchParams;3 let page = parseInt(queryParams.get("page") || "1");4 let limit = parseInt(queryParams.get("limit") || "10");5 6 let allPosts = schema.posts.all();7 let total = allPosts.models.length;8 let start = (page - 1) * limit;9 let end = start + limit;10 11 return {12 posts: allPosts.models.slice(start, end),13 pagination: {14 page,15 limit,16 total,17 totalPages: Math.ceil(total / limit)18 }19 };20});

Working with Models and Relationships

Mirage's model system defines the structure of your mock data and the relationships between different entities. Models are defined in the server configuration and automatically generate CRUD endpoints and relationship methods. The relationship system supports belongsTo, hasOne, hasMany, and belongsToMany associations, enabling you to mock complex data structures.

With relationships defined, you can access related data through the schema, making it possible to mock APIs that return nested or embedded data, which is common in modern REST APIs.

Defining Models with Relationships
1import { createServer, Model, hasMany, belongsTo } from "miragejs";2 3export default createServer({4 models: {5 author: Model.extend({6 posts: hasMany(),7 }),8 post: Model.extend({9 author: belongsTo(),10 comments: hasMany(),11 }),12 comment: Model.extend({13 post: belongsTo(),14 author: belongsTo(),15 }),16 },17});

Authentication and JWT Token Handling

Mocking authenticated endpoints requires special attention to token handling, session management, and authorization checks. Mirage provides the flexibility to implement realistic authentication flows that mirror production behavior. You can verify that your frontend correctly handles tokens, refreshes expired sessions, and redirects unauthenticated users.

The mock authentication also enables testing different user roles and permission levels by issuing tokens with different claims.

JWT Authentication Endpoint
1this.post("/auth/login", (schema, request) => {2 let attrs = JSON.parse(request.requestBody);3 let { email, password } = attrs;4 5 let user = schema.users.findBy({ email });6 7 if (!user || user.password !== password) {8 return new Response(401, {}, {9 errors: ["Invalid email or password"]10 });11 }12 13 let token = generateMockJwt(user);14 15 return {16 user: user.toJSON(),17 token,18 expiresIn: 360019 };20});

Using Factories and Seeds for Realistic Data

Factories and seeds are essential tools for creating realistic test data in Mirage. Factories define how to create records with random but controlled data, while seeds populate the database with initial data when the server starts. The factory system uses sequences and faker functions to generate varied data, ensuring your tests and demos have realistic data without requiring manual creation.

Factories and Seeds Configuration
1export default createServer({2 factories: {3 user: Factory.extend({4 name(i) { return `User ${i}`; },5 email(i) { return `user${i}@example.com`; },6 role(i) { return i % 3 === 0 ? "admin" : "user"; },7 }),8 post: Factory.extend({9 title(i) { return `Post Title ${i}`; },10 published() { return Math.random() > 0.3; },11 }),12 },13 14 seeds(server) {15 server.createList("user", 10);16 server.createList("post", 20);17 server.createList("comment", 50);18 },19});

Best Practices for Production-Ready Mocking

When using Mirage JS in development, follow these practices to ensure your mocks accurately reflect production behavior and don't mask integration issues.

Configure environment-specific behavior: Only enable Mirage in development and test environments. In production, your application should use real API endpoints.

Maintain consistency with the real API: Keep your mock API's behavior consistent with the production API's specification. Document any differences to prevent surprises when connecting to production services.

Use realistic data and timing: Simulating realistic latency and data volumes helps identify performance problems early in development. This practice aligns with our comprehensive web development approach that emphasizes building applications that perform reliably at scale.

Maintain mocks as the API evolves: Treat the mock as a first-class artifact and update it as the real API changes.

Frequently Asked Questions

Can I use Mirage JS with Next.js?

Yes, Mirage JS works with Next.js. Configure it to only run in the browser by checking for typeof window before creating the server, preventing errors during server-side rendering.

How do I switch between mock and real APIs?

Use environment variables or build-time configuration to conditionally create the Mirage server. Your application code remains unchanged, making it easy to switch between environments.

Does Mirage support GraphQL?

Yes, Mirage can mock GraphQL APIs. You can use query resolvers and mutations to handle GraphQL requests with the same flexibility as REST endpoints.

How do I handle CORS in Mirage?

Mirage automatically handles CORS for mocked requests since they never leave the browser. Cross-origin requests to real servers still follow standard CORS rules.

Ready to Build Better Web Applications?

Our team specializes in modern web development with Next.js, creating fast, SEO-optimized applications that scale.