Models in Mirage JS: The Foundation of Mocked Data

Mirage JS brings familiar back-end concepts to front-end development. Models are classes that define the properties of data stored in Mirage's in-memory database. For instance, a user model would specify properties like name and email, and every new user created would follow that definition.

While you can mock data manually, using Mirage's Model class gives you built-in data persistence. It wraps your database and enables relationships that make it easy to return different collections of data to your application. Without models, associations — which we'll explore later — aren't available.

To create a model, import the Model class from Mirage JS:

import { Server, Model } from ‘miragejs’

Then use it in your server options:

let server = new Server({
  models: {
    user: Model,
  }

You don't need to specify properties in advance. Mirage automatically pluralizes model names for the schema, and entries can be created rapidly with flexible fields.

Working with Model Instances

To create entries, use the schema's create() method. In a typical setup, this happens inside the seeds() method of your server:

let user = schema.users.create({name: “Harry Potter”})
let server = new Server({
  models: {
    user: Model
  },

  seeds(server) {
    server.create("user", { name: "Harry Potter" });
});

Access properties and relationships on an instance with dot notation:

let user = schema.users.create({name: “Hermione Granger”})
user.name
// Hermione Granger

If the instance has a relationship, such as posts, you can access it the same way:

user.posts
// Returns all posts belonging to the user 

To find existing instances, use the schema. For example, to retrieve the first of three user instances:

let firstUser = schema.users.find(1)
// Returns the first user

Instance Properties

Mirage exposes useful properties on model instances. The associations property returns a hash of all relationships belonging to that instance:

let harry = schema.users.create({name: “Harry Potter”})
user.associations
// would return associations of this instance if any

The attrs property returns all fields or attributes of a specific instance:

harry.attrs
// { name: “Harry Potter” }

Instance Methods

Several methods are available on each model instance:

  • destroy() — removes the instance from Mirage's database:
harry.destroy()
  • isNew() — returns true if the model hasn't been persisted yet. Since create() always saves, isNew() returns false for those instances. If you use the new method without calling save(), it returns true:
let ron = schema.users.new({name: “Ronald Weasley”})

ron.isNew()

// true

ron.save()

ron.isNew()

// false
  • isSaved() — the opposite of isNew(); checks whether an instance has been saved to the database.
  • reload() — reloads an instance from the database, but only works if the instance was saved. Useful for refreshing actual attribute values after local changes:
let headmaster = schema.users.create({name: “Albus Dumbledore”})

headmaster.attrs
// {id: 1, name: “Albus Dumbledore”}

headmaster.name = “Severus Snape”

headmaster.name
// Severus Snape

headmaster.reload()

headmaster.name

// Albus Dumbledore
  • save() — persists or creates a record in the database. Only needed when you created an instance without using create():
let headmaster = schema.users.new({name: “Albus Dumbledore”})

headmaster.id
// null

headmaster.save()

headmaster.name = “Severus Snape”
// Database has not yet been updated to reflect the new name

headmaster.save()
// database has been updated

headmaster.name

// Severus Snape
  • toString() — returns a string representation of the model and its id:
headmaster.toString()

The result looks like:

// “model:user:1”
  • update() — updates a specific instance in the database. It takes two arguments: the key as a string, and the new value:
let headmaster = schema.users.find(1)
headmaster.update(“name”, “Rubeus Harris”)

Associations: Defining Relationships Between Models

Associations define relationships between models — either one-to-one or one-to-many. They're a powerful way to fetch a model along with its related data, such as a user and all their posts.

Mirage provides two association helpers:

  • hasMany() — defines to-many relationships.
  • belongsTo() — defines to-one relationships.

When you use either helper, Mirage automatically injects useful properties and methods into the associated model instances.

Using belongsTo()

First, import belongsTo:

import { Server, Model, belongsTo } from 'miragejs'

Then define your models with relationships using the extend method:

new Server({
  models: {
    post: Model.extend({
      author: belongsTo(),
    }),

    author: Model,
  },
})

This establishes a to-one relationship from the post model to an author model. With that in place, you can access related data directly:

post.authorId // returns the author id of the post
post.author // Author instance
post.author = anotherAuthor
post.newAuthor(attrs) // creates a new author without saving to database
post.createAuthor(attrs) // creates a new author and save to database

Using hasMany()

Import hasMany just like belongsTo:

import { Server, Model, hasMany } from 'miragejs'

Then create your to-many relationships:

  models: {
    post: Model.extend({
      comments: hasMany(),
    }),

    comment: Model,
  },
})

Like belongsTo(), the hasMany() helper automatically adds properties and methods to the affected models:

post.commentIds // [1, 2, 3]
post.commentIds = [2, 3] // updates the relationship
post.comments // array of related comments
post.comments = [comment1, comment2] // updates the relationship
post.newComment(attrs) // new unsaved comment
post.createComment(attrs) // new saved comment (comment.postId is set)

With models and associations, you can build a realistic mocking layer that supports complex data relationships without an actual backend. This sets the stage for factories, fixtures, and serializers, which we'll cover in the next part of this deep dive series.