DEV Community

Cover image for Mocking Back-ends for React Apps with MirageJS
Anik
Anik

Posted on

Mocking Back-ends for React Apps with MirageJS

Seven tips to help you avoid bottlenecks and future-proof your mock back-end


⚠️ Disclaimer
This article assumes you have some familiarity with the React framework, and Javascript and Typescript fundamentals. It also assumes you are comfortable with concepts and terms related to relational databases.

Table Of Contents


What led me to consider Mirage

I recently helped create a high-fidelity React-based prototype for a new product still in its early stages. We were not entirely certain if the product would solve the problem it was intended to solve for our customers. We needed feedback and, in order to get that feedback, we needed to put something in front of potential customers that wasn’t merely a slideshow or clickable mockup, something closer to the “real thing”. We wanted them to be able to explore and interact with the prototype in a much more realistic manner than they otherwise could if it were just a slideshow or mockup with limited interaction targets, and a strict, pre-determined “flow”. This would ultimately help us decide whether we could continue building out the rest of the application as-is, or if we needed to go back to the drawing board.

Building such a prototype, without an actual back-end, would require significantly fewer resources and would therefore pose less of a risk to our organization in terms of cost, should the concept prove to be unviable. But how does one go about building a UI prototype with realistic data, and simulating the ability to interact with and modify said data, without an actual back-end? This is the question that first led me to consider a library like Mirage.

Mirage (and other tools like Mirage) allow us to mock back-ends and APIs for JavaScript applications. With a mock data layer, fixed and/or dynamically generated mock data, and an accompanying API, you can build your client application, simulate the accessing and modifying of data, and even run tests, as though the back-end and API already exist.

I don’t plan on using this article exclusively to try and convince you to use Mirage on a future project. I think the MirageJS documentation already makes a great case as to the “why”. The documentation also has excellent step-by-step guides and tutorials, so this article is not a “getting started” guide, either. Instead, I plan to use this opportunity to share what I feel are some good “best practices”, drawn from my own experiences.


Don’t put everything in one file

To start off, it’s entirely possible to define and configure the entire mock back-end in one massive function inside a server.ts file (or server.js file, if you’re not using Typescript). In fact, if you follow Mirage's tutorials, that's basically what they’ll have you do initially: all of the code for the mock back-end — models, routes and route handlers, fixtures, seed data factories, and everything else — in one file. However, I've found that this approach becomes cumbersome in the long-run, especially once the mock back-end becomes more complex and once you start adding custom route handlers.

Here is how I like to organize my Mirage code:

📁 src/ (root directory of React app)
 ├── 📁 components/   ─┐
 ├── 📁 hooks/         ├── React app components and
 ├── 📁 ...           ─┘    other client app code
...
 └── 📁 mock-api/
      ├── 📁 models
      ├── 📁 factories
      ├── 📁 routes
      ├── 📁 serializers
      ├── 📁 clients
     ...
      ├── 📄 server.ts
      └── 📄 index.ts
Enter fullscreen mode Exit fullscreen mode

I’ll be going over the individual pieces in more detail shortly, but here’s a general summary:

  • I keep all of the Mirage mock back-end code inside a 📁 mock-api (or similarly named) directory.
  • I keep the main 📄 server.ts file (where my Mirage server instance generator function is defined) directly inside this directory.
  • The 📁 models directory is where I store Typescript interfaces for all data models, as well as other interfaces, enums, and other type declarations related to the data.
  • The 📁 factories directory contains sample data fixtures for seeding the mock database with initial sample data. It also contains any factories responsible for generating seed data with custom generator functions. I could have also named this folder "data".
  • The 📁 routes directory contains code that implements custom route handlers and registers routes with the server instance. For instance, if I defined a custom HTTP GET endpoint that allowed a list of items to be searched, sorted, and filtered, I would place that code inside this directory. The 📁 serializers directory is for any custom data serializers that I’ve defined, in case I find it necessary to serialize certain data in a specific way after processing queries. The topic of serializers is beyond the scope of this article, but the Mirage documentation has plenty of information on the subject.
  • The 📁 clients directory contains API client functions that simplify the process of sending API requests from the front-end, and reduce the amount of repeated boilerplate code I have to write when doing so.

The advantages of splitting out code in this fashion should be fairly self-explanatory, but two things in particular stand out to me:

  • When committing changes to the Mirage code, diffs will be far easier to understand and interpret. Instead of viewing a hodgepodge of changes within one massive file, changes will appear more surgical, split out over several files. It should be easier to tell what changed and what didn’t, and easier to spot mistakes.
  • Maintenance becomes easier. Avoid having to scroll up and down an enormous file to make several related changes or to compare two or more related things. It’s much easier to locate something by pulling up a separate, aptly-named file, than by hunting for it inside thousands of lines of code.

Start with a plan

For Mirage, “models” (similar to tables in a relational database) are the building blocks of the mock back-end; they define the schema on which everything stands. Before getting too far ahead, I recommend taking some time to plan out the models and relationships between those models.

It’s very easy to get started with Mirage and I initially made the mistake of running before I could walk. I hastily defined the models and relationships thereof, and started building the mock back-end and the various route handlers. I soon discovered that I had overlooked some crucial aspects of the schema, and ended up spending hours reworking the models, seed data, and other related things.

In the example below, I’ve defined some models for a hypothetical application's mock back-end, via the models config property.

import { createServer, Model } from "miragejs";

export function makeServer() {
  const server = createServer({
    models: {
      product: Model,
      order: Model,
      orderItem: Model,
      tag: Model,
      user: Model,
      userFavorite: Model,
    },
  });
  return server;
}
Enter fullscreen mode Exit fullscreen mode

These models are not very useful at the moment. As you may suspect just by reading their names, there are some relationships between these distinct models. Multiple OrderItems, for instance, comprise and are related to one Order. An item marked as a UserFavorite is related to a particular Product and User. To see the relationships between these models more clearly, let’s first create a simple schema:

Simple schema for mock back-end

This, of course, is a highly-simplified, contrived example. The benefits of such preparation become more obvious when grappling with complex, realistic schemas. The Mirage API provides easy ways to define relationships (one-to-many, many-to-one, etc.) between models, using the belongsTo() and hasMany() helpers. Let’s configure our models to reflect those relationships, now that we know what they are:

export function makeServer() {
  const server = createServer({
    models: {
      product: Model.extend({
        tag: hasMany(),
      }),
      order: Model.extend({
        user: belongsTo(),
      }),
      orderItem: Model.extend({
        order: belongsTo(),
        product: belongsTo(),
      }),
      tag: Model.extend({
        product: hasMany(),
      }),
      user: Model.extend({
        userFavorite: hasMany(),
      }),
      userFavorite: Model.extend({
        user: belongsTo(),
        product: belongsTo(),
      }),
    },
  });
  ...
}
Enter fullscreen mode Exit fullscreen mode

Mirage will automatically assign primary and foreign keys for each model, based on how you’ve configured the relationships. When accessing UserFavorite data in the mock database, for instance, you’ll find that each UserFavorite record now has userId and productId fields that serve as foreign keys that correspond to the primary keys of particular instances of a User and a Product in the database, respectively.


Define Typescript interfaces for models (if your project implements Typescript)

For obvious reasons, if your application does not implement Typescript this tip probably won’t be very useful in your particular case.

In the previous example, the models we defined will allow us to take advantage of Mirage’s object-relational mapping (ORM) capabilities. When running queries on the mock database, we will serialize the results into JSON and convey them back to the React app, simulating exactly what would happen with an actual back-end with a web API. The JSON result must then be deserialized in the client app before the data can be processed by the React components. Assuming the React app implements Typescript, wouldn’t it be nice if the components had access to interfaces and types that defined the structure of the data? Plus, if certain components needed to pass around data via props, we could use said interfaces to declare prop types.

The schema we defined earlier will come in handy for this. We can now easily declare Typescript interfaces for each of the models in our mock back-end. Let’s start with the Tag and Product models:

Deriving Typescript interfaces from the schema models

We know there’s a many-to-many (both-ends-optional) relationship between products and tags, as one tag could be associated with one, multiple, or zero products, and each product could be associated with one tag, many tags, or no tags at all. Actually, because we suggested a many-to-many relationship in the model configuration, Mirage will automatically add a property to each Tag in the database that tracks all of the related products associated with that tag. But we don’t necessarily want an array of Products for each Tag object, as far as our client app is concerned.

The Tag model’s corresponding interface is pretty simple. As for the Product interface, each product object will contain an array of tags. Each member in this array is a Tag object.

Now let’s say our hypothetical UI will display products in a list, with each individual product shown as a card containing that product’s information. Let’s say that these cards are rendered using some ProductCard component:

interface ProductCardProps {
  name: string;
  seller: string;
  price: number;
  type: string;
  tags: string[];
}
function ProductCard(props: ProductCardProps) {
  return (
    ...
  );
}
Enter fullscreen mode Exit fullscreen mode

In another part of the UI, a list of “recommended products” is displayed, with minimal information about each product. This RecommendedProduct component might be used to display each product snippet:

interface RecommendedProductProps {
  name: string;
  seller: string;
  price: number;
}
function RecommendedProduct(props: RecommendedProductProps) {
  return (
    ...
  );
}
Enter fullscreen mode Exit fullscreen mode

There could be many more components like these in the app, each displaying or processing product data in some form or fashion. I’ve purposefully omitted their implementation details, because right now we’re more concerned with their props APIs. The props interfaces shown are built to match only the current structure of product data in the mock back-end, and they feature code duplication.

What happens if we change the schema, so that products no longer had a seller property? What if we renamed the price property to cost? We would need to remember all of the components that handle product information, and then update each of them individually every time such a change occurred. We can avoid this by utilizing the Product interface we defined just a while ago. We’ll have the individual components' props interfaces extend that “base” type for the product data model.

import { Product } from "./path/to/mock-api-code";
...
interface ProductCardProps extends Product {
  // additional component-specific props not present on the base Product model
  ...
}
...
interface RecommendedProductProps
  extends Pick<Product, "name" | "seller" | "price"> {
  // additional component-specific props not present on the base Product model
  ...
}
Enter fullscreen mode Exit fullscreen mode

Now, whenever the structure of product data in our mock back-end changes, we only have to update the Product interface to reflect that change. This update will also be reflected in any interface that extends the base Product interface, whether entirely or in part. For the RecommendedProductProps interface, we only care about a select few properties of the Product, so we’ll use the Pick utility type to extend a subset of the Product interface with just those properties.

I like to place to these interfaces and other related types in separate files, categorized more-or-less by some “domain” to which I believe they belong. As shown earlier, I usually start with a 📁 models directory inside the 📁 mock-api directory. In this 📁 models directory, I then create sub directories for each distinct domain, like so:

📁 src/ (root directory of React app)
 ├── 📁 ...
 └── 📁 mock-api/
      ├── 📁 models
      │    ├── 📁 products
      │    │    ├── 📄 product.ts
      │    │    ├── 📄 product-type.ts
      │    │    └── 📄 index.ts
      │    ├── 📁 orders
      │    ├── 📁 order-items
      │   ...
      │    └── 📄 index.ts
     ...
      ├── 📄 server.ts
      └── 📄 index.ts
Enter fullscreen mode Exit fullscreen mode

If you look back at the Product interface we defined a while ago, you’ll notice it has a property, type, whose value is a ProductTypeEnum. This Typescript enum does not correspond to any model that our mock back-end cares about; the enum values become integers as far as the mock data or any data transfer objects are concerned. But the enum type will still be useful for the React components in the front-end. Because the two are so closely related in this way, I consider both the Product interface and the ProductTypeEnum to be part of the same Product domain. The enum is defined inside the 📄 product-type.ts file and the other interface is defined inside 📄 product.ts; I've purposefully grouped these two files together.


Consider a “hybrid” approach for generating realistic seed data

One of the key requirements for my UI prototype was that the sample data needed to be as realistic as possible. This involved simulating real customer scenarios, acquiring actual addresses and GPS coordinates, and so on. It’s not very common have such an extreme level of detail in a prototype, but the experience did force me to figure out creative ways to efficiently generate and handle seed data for a mock back-end.

Mirage allows you to configure initial data for a server instance by defining seed data fixtures, using the fixtures config option. Because the sheer amount of initial sample data for a realistic prototype could get very large (as was the case for my UI prototype), I like to keep the sample data fixtures in a separate file and them inject them into the createServer() function. In the example below, when the server instance is first loaded, the mock database will be seeded with the following product data:

/* product-data.ts */
import { Product } from "path/to/models";

export const products: Product[] = [
  { 
    id: 1, 
    name: "Brown Leather Jacket", 
    seller: "Acme Apparel", 
    ... 
  },
  { 
    id: 2, 
    name: "Inflatable Pool", 
    seller: "Bravo Recreation", 
    ... 
  },
  ...
  { 
    id: 10, 
    name: "Small Notepad", 
    seller: "Jones Suppliers", 
    ... 
  },
];
Enter fullscreen mode Exit fullscreen mode

Now we just need to pass in the products array to the fixtures config option (N.B. — remember to use the singular form, product, when defining models, and the plural form, products, when passing in fixtures):

import { products } from "path/to/sample-data";

export function makeServer() {
  const server = createServer({
    models: {
      product: Model.extend({ ... }),
      ...
    },
    fixtures: {
      products,
    },
    ...
  });
  return server;
}
Enter fullscreen mode Exit fullscreen mode

Fixtures are great if you’re willing to write out seed data by hand. For obvious reasons, this won’t be a very good use of your time in more complex scenarios. If you need to generate 1,000 or 10,000 rows of sample data, for instance, it’s better to find something that can generate the seed data for you instead.

Luckily, Mirage allows you to accomplish this pretty easily, using factories. Once again, I like to keep factories in a separate place and inject them into the server generator function. The faker.js library comes in really handy for generating all kinds of mock data — from names, to phone numbers, to addresses and more. In the examples below, a productFactory is being used to generate 2,000 Product records as initial seed data for the mock database. Custom providers in the productFactory assign product names, seller names, and other product information using mock data generated by faker modules:

/* product-factory.ts */
import { Factory } from "miragejs";
import { commerce, company } from "faker";

export const productFactory = Factory.extend({
  name(): string {
    return commerce.productName();
  },
  seller(): string {
    return company.companyName();
  },
  price(): number {
    return Math.floor(Math.random() * 300) + 20;
  },
  ...
});
Enter fullscreen mode Exit fullscreen mode
import { productFactory } from "path/to/sample-data";

export function makeServer() {
  const server = createServer({
    models: { ... },
    factories: {
      product: productFactory,
    },
    seeds:(_server) {
      _server.createList("product", 2000),
    },
    ...
  });
  return server;
}
Enter fullscreen mode Exit fullscreen mode

While this works fine if you need to generate reams of randomized data (great for stress-testing UI components like tables or lists), it still poses a challenge for realistic prototypes. By simply letting all of the product names, sellers, and prices be randomized, we may end up with some very strange combinations, like $30 cars sold by a “Greenwich Consulting, LLC.”, or $200 bananas sold by “Tim’s Party Supplies”, and so on. Depending on your context, you may or may not care about this level of detail. But if you do care, you may decide that, while some aspects of the seed data can be randomized, other aspects ought to remain more tightly controlled.

Let’s say that I wanted some finer control over the names and prices of products, but I was okay with randomizing the seller names and other properties of the product. Here is a “hybrid” approach that will allow me to define just the properties I want direct control over and let the factory handle the rest. I start by creating an array of sample data, but I only provide values for those properties which I want to control directly. I also expose a count of the number of items in the array.

/* product-data.ts */
import { Product } from "path/to/models";

export const products: Pick<Product, "name" | "price">[] = [
  { name: "Brown Leather Jacket", price: "54.99" },
  { name: "Inflatable Pool", price: "89.99" },
  ...
  { name: "Small Notepad", price: "3.49" },
];

export const productCount = products.length;
Enter fullscreen mode Exit fullscreen mode

Next, I head over to the factory, where I keep things the same for properties being randomly generated, but change how the other properties are handled. When the productFactory iterates over the requested number of items it is asked to generate, it will track the specific index of each generated record. I can use that index to grab information for the product at that same index in my pre-defined list. If, for whatever reason, there isn’t anything at that index in my pre-defined list (this can happen if the factory is asked to generate more records than the number of records in my pre-defined list), I can have the factory fall back to a randomly-generated value instead:

import { commerce, company, datatype } from "faker";
import { products } from "./product-data";

export const productFactory = Factory.extend({
  name(index: number): string {
    return products[index]?.id || commerce.productName();
  },
  seller(): string {
    return company.companyName();
  },
  price(index: number): number {
    return products[index]?.price || Math.floor(Math.random() * 300) + 20;
  },
  ...
});
Enter fullscreen mode Exit fullscreen mode

Lastly, we’ll go back to the createServer() function. Instead of generating 2,000 product records, we’ll only generate as many as we have data for. Remember how we derived productCount from the length of the products array earlier? We can now make use of that:

import { productCount, productFactory } from "path/to/mock-data";

export function makeServer() {
  const server = createServer({
    models: { ... },
    factories: {
      product: productFactory,
    },
    seeds:(_server) {
      _server.createList("product", productCount),
    },
    ...
  });
  return server;
}
Enter fullscreen mode Exit fullscreen mode

The obvious drawback of this hybrid approach is that it can still be painful to generate large sets of mock data, since you’re having to define at least one or more properties by hand for each record. It’s best for scenarios where you may want a sizeable set of sample data, but you don’t necessarily need thousands of rows of said data.

I like to organize sample data fixtures and factories together in a manner similar to how I organize interfaces for data models:

📁 src/ (root directory of React app)
 ├── 📁 ...
 └── 📁 mock-api/
      ├── 📁 factories
      │    ├── 📁 products
      │    │    ├── 📄 product-data.ts
      │    │    ├── 📄 product-factory.ts
      │    │    └── 📄 index.ts
      │    ├── 📁 orders
      │    ├── 📁 order-items
      │   ...
      │    └── 📄 index.ts
     ...
      ├── 📄 server.ts
      └── 📄 index.ts
Enter fullscreen mode Exit fullscreen mode

Split up and aggregate route handlers

Similar to the models, seed data fixtures, and factories, I like to keep custom route handlers in separate files and inject them into the server instance generator function.

📁 src/ (root directory of React app)
 ├── 📁 ...
 └── 📁 mock-api/
      ├── 📁 routes
      │    ├── 📁 user-favorites
      │    │    ├── 📄 get-favorites-by-user.ts
      │    │    ├── 📄 add-favorite-product.ts
      │    │    ├── 📄 remove-favorite-product.ts
      │    │    └── 📄 index.ts
      │    ├── 📁 orders
      │    ├── 📁 order-items
      │   ...
      │    └── 📄 index.ts
     ...
      ├── 📄 server.ts
      └── 📄 index.ts
Enter fullscreen mode Exit fullscreen mode

Each custom route handler gets its own separate file. For instance, I’ve defined a custom route handler that allows a user to designate a product as one of their favorites. The implementation of this route handler is in the 📄 add-favorite-product.ts file.

The manner of organization here may invite some questions: do route handlers for adding/removing a product to a user’s favorites belong in the “products” domain or in the “user-favorites” domain? The current organization seems to suggest the latter. In this hypothetical scenario, when designating products as a user favorite, we’d most likely call a PUT endpoint at the route .../api/user-favorites/some-product-id. When removing a product from the user favorite list, we would call a DELETE endpoint at the same route. As this topic is beyond the scope of this article, I won't venture too far into the weeds here.

Certain custom route handlers (e.g. a POST or PUT endpoint that modifies a user’s account info) may require specific request payloads. I like to define Typescript interfaces for all data transfer objects, whether they are the request payloads provided to route handlers or the response objects returned. I typically keep these interfaces alongside the route handlers to which they are related. These interfaces can be exposed to client app components that call the related API endpoints, greatly increasing reliability with stricter type controls.

To ensure that my custom route handlers will get called when API calls are made, I must first ensure the routes are being registered with the server instance. Let’s take a look inside the 📄 index.ts file in the 📁 user-favorites directory:

/* routes/user-favorites/index.ts */
import { Server } from "miragejs";
import { getFavoritesByUser } from "./get-favorites-by-user";
import { addFavoriteProduct } from "./add-favorite-product";
import { removeFavoriteProduct } from "./remove-favorite-product";

export function registerUserFavoritesRoutes(context: Server) {
  return [
    context.get(
      "/user-favorites/user/:userId/", 
      getFavoritesByUser, 
      { timing: ... }
    ),
    context.post(
      "/user-favorites/:productId", 
      getFavoritesByUser
    ),
    context.delete(
      "/user-favorites/:productId", 
      getFavoritesByUser
    ),
  ];
}
Enter fullscreen mode Exit fullscreen mode

I register each individual route handler with the server context, which is passed in as the lone parameter of the function. All that’s left to do is to give this function to createServer() so that the server registers these routes upon instantiation. We’ll need to pass a value for the context parameter. To do this, we simply pass in a reference to the server instance — this:

import { registerUserFavoritesRoutes } from "../routes";

export function makeServer() {
  const server = createServer({
    models: { ... },
    factories: { ... },
    seeds: { ... },
    routes: {
      registerUserFavoritesRoutes(this);
    },
    ...
  });
  return server;
}
Enter fullscreen mode Exit fullscreen mode

Create API client functions for UI components

I like to decouple the mock back-end from the client app as much as possible to keep the front-end lean and efficient. I define API “client functions” that serve as convenience wrappers around the actual fetch (or axios, or whatever) call, like the addUserFavorite() function shown below. These functions provide simple APIs for components to use. All of the functionality for actually making the API call and returning the results is contained within the functions themselves.

async function addUserFavorite(
  userId: string, 
  productId: string
): Promise<UserFavoriteDTO> {
  try {
    const response = await fetch(`/mock-api/user-favorites/${productId}`, {
      method: "PUT",
      cache: "no-cache",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        userId,
      });

      if (response.ok) {
        return response.json() as Promise<UserFavoriteDTO>;
      }
      throw new Error(...);
    });
  } catch (reason) {
    ... // Handle other errors
  }
}
Enter fullscreen mode Exit fullscreen mode

I aggregate all the client functions for a particular domain inside a single object, and then expose this object to the front-end:

const userFavoritesClient = {
  list: getUserFavorites,
  add: addUserFavorite,
  remove: removeUserFavorite,
};
export default userFavoritesClient;
Enter fullscreen mode Exit fullscreen mode

Let’s say that users can add products to their list of favorites by clicking some button next to a product’s information card. The component responsible for that interaction needs to be able to call the API endpoint to designate a new “favorite product”. All we have to do now is call the function we just defined. Any information necessary to perform the request (information which could determine the actual route or which could comprise a request payload) can be passed in as parameter(s):

import { userFavoritesClient } from "./mock-api";

function SomeComponent(props: SomeComponentProps) {
  const { productId, ... } = props;
  const { userId, ... } = useContext(...);
  ...
  async function addFavorite() {
    try {
      await userFavoritesClient.add(userId, productId);
      ...
    } catch (reason) {
      ...
    }
  }
  ...
  return (
    ...
      <Button onClick={addFavorite}>
        Add to Favorites
      </Button>
    ...
  );
}
Enter fullscreen mode Exit fullscreen mode

The component doesn’t need to concern itself with which specific route needs to be called or what API base URL ought to be used. The developer doesn’t have to worry about writing tedious fetch calls every time, properly serializing request payloads, or deserializing responses. The code is clean and streamlined.

My way of organizing the client functions is pretty rudimentary: I put all API client functions related to a particular domain in one file and expose one “client” object from each file, as shown earlier. I place all of these files in one 📁 clients directory:

📁 src/ (root directory of React app)
 ├── 📁 ...
 └── 📁 mock-api/
      ├── 📁 clients
      │    ├── 📄 products-client.ts
      │    ├── 📄 orders-client.ts
      │    ├── 📄 user-favorites-client.ts
      │   ...
      │    └── 📄 index.ts
     ...
      ├── 📄 server.ts
      └── 📄 index.ts
Enter fullscreen mode Exit fullscreen mode

Set up passthroughs and a unique namespace for mock routes

There are several reasons why you may want to have a mock back-end coexist with an actual back-end, even if temporarily and under specific circumstances. During development, you may want to have live data flowing into some parts of an application while other parts, especially those still under construction, remain connected to a mock back-end with sample data. You might be concerned with mocking up only a portion of the back-end that doesn’t exist yet, while the remainder of the application remains wired up.

We did eventually start building out the actual back-end. The sample data, when the application was still a UI prototype, was of very high quality and greatly facilitated demos and discussions with customers. We found that setting up the same amount of realistic data in the actual back-end would take days, maybe weeks. For this and various other reasons, we decided to keep a “demo” mode for the application:

  • When the demo mode was disabled, the application would display live data. The application’s API calls would hit the actual backend and queries would be performed on the actual database.
  • Enabling the demo mode would result in the sample data being displayed. API calls would be intercepted by Mirage and the mock database instance would instead be the target for all queries performed.

By default, Mirage intercepts all outgoing HTTP requests in a manner similar to monkey-patching, disallowing communication with web APIs at the same time. To allow some requests to pass through, you must explicitly declare passthrough routes. This is done in the routes config property which we used earlier to inject custom route handlers. I tend to put this all the way towards the end:

import { injectUserFavoritesRoutes } from "../routes";

export function makeServer() {
  const server = createServer({
    models: { ... },
    factories: { ... },
    seeds: { ... },
    routes: {
      ...
      this.passthrough((request) => {
        // Custom comparator function
        // Return true if Mirage should allow the request
        // to pass through, or false if it should be
        // intercepted
        return request.url.includes("api/v1");
      });
    },
    ...
  });
  return server;
}
Enter fullscreen mode Exit fullscreen mode

In the above example, Mirage will not intercept any HTTP requests that include api/v1 as part of the request URL. You can also pass in fully-qualified domain names if any API requests will go to some external domain, and you can provide as many passthrough route declarations as you want:

const server = createServer({
  ...
  routes: {
    ...
    this.passthrough("https://localhost:9001/api/**");
    this.passthrough("https://external-domain-one.com/api/**");
    this.passthrough("https://api.external-domain-two.net/v1/**");
    ...
  },
  ...
});
Enter fullscreen mode Exit fullscreen mode

I also like to include one additional thing — a custom namespace for the mock API routes:

const server = createServer({
  ...
  routes: {
    this.namespace = "/mock-api";
    ...
    this.passthrough(...);
    ...
  },
  ...
});
Enter fullscreen mode Exit fullscreen mode

In the front-end, calls to the mock back-end will now include mock-api as part of the route, to distinguish them from calls to the actual back-end (which we will allow to pass through).


Closing thoughts

The recommendations herein likely reflect some of my personal biases. They are not meant to be rules or exhaustive guidelines by any means. There’s so much more to Mirage; I've only scratched the proverbial surface, but I’ve learned quite a few things along the way. I share these tips in the hopes that, whether you’re new to Mirage or already quite familiar with it, they might come in handy for you as well.

There are even more tips and useful methodologies I'd love to share with you, but I fear this article would become far too long if I tried to cram them all in here. I have plans for an npm package with some useful functions and hooks I've built that make the process of generating realistic mock data less of a hassle. I'll have more details on that in the near future.

Is there anything in here you particularly liked? Do you have concerns or suggestions for improvement, or have you spotted any errata? Have you worked with Mirage before and have some ideas or approaches you’ve found useful which were not mentioned here? Please leave a comment!

Thanks for reading!

Discussion (3)

Collapse
lioness100 profile image
Lioness100

This was a long read but worth every second. Mocking back-ends has always been troubling for me, so thanks for this!

Collapse
anikcreative profile image
Anik Author

Thank you so much! I had to crop it at some point because it was becoming so lengthy, lol. There's so much more but I wanted to make sure I got across the stuff I felt was the most important.

Collapse
lioness100 profile image
Lioness100

I think it was perfect 💖