Blueprinting Mock Data: Factories in Mirage JS
When mocking an API with Mirage JS, route handlers intercept requests and return data from Mirage's in-memory database. A simple route handler for api/products initially returns an empty collection because no records exist yet.
import { Server, Model } from 'miragejs';
new Server({
models: {
product: Model,
},
routes() {
this.namespace = "api";
this.get('products', (schema, request) => {
return schema.products.all()
})
}
});
},
Seeding the database manually via the seeds method works for a handful of records, but it doesn't scale. Writing out hundreds or thousands of individual objects by hand is impractical. That's where factories come in.
A factory is a blueprint for generating records for a specific model. Instead of defining each record inline, you define the shape of the data once, then instantiate as many records as you need. Factories are declared on the Server instance under a factories property, with each factory keyed by the model name it produces.
import { Server, Model, Factory } from 'miragejs'
new Server({
models: {
product: Model
},
factories: {
product: Factory.extend({
name(i) {
// i is the index of the record which will be auto incremented by Mirage JS
return `Awesome Product ${i}`; // Awesome Product 1, Awesome Product 2, etc.
},
price() {
let minPrice = 20;
let maxPrice = 2000;
let randomPrice =
Math.floor(Math.random() * (maxPrice - minPrice + 1)) + minPrice;
return `$ ${randomPrice}`;
},
category() {
let categories = [
'Electronics',
'Computing',
'Fashion',
'Gaming',
'Baby Products',
];
let randomCategoryIndex = Math.floor(
Math.random() * categories.length
);
let randomCategory = categories[randomCategoryIndex];
return randomCategory;
},
rating() {
let minRating = 0
let maxRating = 5
return Math.floor(Math.random() * (maxRating - minRating + 1)) + minRating;
},
}),
},
})
Factory attributes can be static values — strings, numbers, booleans — or functions that return dynamic data, as shown above with Math.random. This flexibility makes factories suitable for creating realistic, varied mock datasets.
To create a single record from a factory, call server.create('product'). Mirage uses the matching factory to populate the new record. You can inspect the database contents with console.log(server.db.dump()).
new Server({
seeds(server) {
server.create("product")
}
})
Overrides and Bulk Creation
Factory defaults can be overridden on a per-record basis by passing explicit values to server.create. This is useful when you need a specific record that deviates from the standard blueprint.
server.create("product", {name: "Yet Another Product", rating: 5, category: "Fashion" })
For bulk record creation, Mirage provides server.createList. This method takes the model name and a non-zero positive integer as arguments:
server.createList("product", 10)
This pattern is particularly valuable for UI testing, where you often need to simulate scenarios that involve many records of the same type.
Fixtures: Static Baseline Data
Fixtures serve a different purpose than factories. In testing terminology, a fixture is a known state of data used as a baseline to ensure repeatable test results. Mirage lets you define fixtures that are automatically loaded into the database as initial data.
fixtures: {
products: [
{ id: 1, name: 'T-shirts' },
{ id: 2, name: 'Work Jeans' },
],
},
One important caveat: if a seeds function is defined on your server, Mirage ignores fixtures, assuming you intend to seed data via factories instead. To use fixtures alongside factory-based seeding, call server.loadFixtures() explicitly.
fixtures: {
products: [
{ id: 1, name: "iPhone 7" },
{ id: 2, name: "Smart TV" },
{ id: 3, name: "Pressing Iron" },
],
},
seeds(server) {
// Permits both fixtures and factories to live side by side
server.loadFixtures()
server.create("product")
},
For cleaner project organization, fixtures should live in separate files — for example, a fixtures/products.js file — and be imported into your server configuration.
// <PROJECT-ROOT>/fixtures/products.js
export default [
{ id: 1, name: 'iPhone 7' },
{ id: 2, name: 'Smart TV' },
{ id: 3, name: 'Pressing Iron' },
];
In practice, factories are generally the preferred choice. They require fewer keystrokes, are less verbose, and keep mocks more maintainable. Fixtures are best reserved for specific use cases where a static, known data state is required by design.
Serializers: Shaping the JSON Payload
Factories and fixtures populate the database, but the response format your frontend receives is controlled by serializers. A serializer transforms a model or collection returned from a route handler into the JSON structure your application expects.
this.get('products/:id', (schema, request) => {
return schema.products.find(request.params.id);
});
Mirage ships with three built-in serializers:
JSONAPISerializer— follows the JSON:API spec.ActiveModelSerializer— mimics APIs built with the Rubyactive_model_serializersgem.RestSerializer— a general-purpose "catch all" for common REST APIs.
import { Server, RestSerializer } from "miragejs"
The RestSerializer is used by default, so explicitly declaring it is optional but shown here for clarity. The choice among serializers depends on the format of the real backend you're mocking.
import { Server, JSONAPISerializer } from "miragejs"
new Server({
serializers: {
application: JSONAPISerializer,
},
})
The JSONAPISerializer wraps data according to the JSON:API specification. The ActiveModelSerializer demonstrates its value through attribute naming conventions. If a factory attribute is named productCategory, this serializer transforms it into product_category in the output, aligning with Rails conventions.
import { Server, ActiveModelSerializer } from "miragejs"
new Server({
serializers: {
application: ActiveModelSerializer,
},
})
Custom Serializer Behavior
Built-in serializers can be extended to match specific frontend requirements. For instance, if your application expects camelCase attribute names, you can override the default behavior. Using the lodash utility library, you can customize the serializer to apply the camelCase transform to all keys:
import { RestSerializer } from 'miragejs';
import { camelCase, upperFirst } from 'lodash';
serializers: {
application: RestSerializer.extend({
keyForAttribute(attr) {
return upperFirst(camelCase(attr));
},
}),
},
This produces a JSON payload where attributes are properly camelCased, demonstrating that serializers are not fixed — they can be tailored to fit the exact API contract your frontend code assumes.



