Mirage.js

demo.env

Przemek Suchodolski @przemuh

Agenda

  1. What is mirage.js ? (walkthrough)
  2. Demo Mode (use case)

About me

  • @przemuh / przemuh.dev
  • Frontend dev since 2012
  • πŸ‡°πŸ‡· corp, πŸ‡΅πŸ‡± software-house, πŸ‡ΊπŸ‡Έ startup

  • Musician amateur 🎸 πŸ₯ 🎹 (soundcloud.com/przemuh)

Mirage.js

Build complete frontend features,
even if your API doesn't exist.

Sam Selikoff

@samselikoff

Ryan Toronto

@ryantotweets

  • 2015 - Pretender.js
  • 2015 - ember-cli-pretenderify
  • ember-cli-mirage
  • 2019 mirage.js (standalone)

Β 

How to ...πŸ€”

  • develop frontend & backend in parallel
  • mock backend services in tests
  • share working app prototype without backend
  • fakeAPI
  • MSW
  • JSON Server
  • Interceptors (Pretender.js, cy.intercept())

Data Management

  1. Network request interceptor (Pretender.js)
  2. Database (in-memory)

Mirage.js works as...

  • Relationships
  • ORM
  • Factories
import React from "react"
import ReactDOM from "react-dom"
import App from "./App"
import { createServer } from "miragejs"

createServer({
  routes() {
    this.namespace = "api"

    this.get("/todos", ({ db }) => {
      return db.todos
    })
  },
})

ReactDOM.render(<App />, document.getElementById("root"))
import { createServer, Model } from "miragejs"

createServer({
  models: {
    blogPost: Model,
  },
})
import { createServer, Model, belongsTo } from "miragejs"

createServer({
  models: {
    blogPost: Model.extend({
      author: belongsTo(),
    }),

    author: Model,
  },
})
import { createServer, Model, belongsTo, hasMany } from "miragejs"

createServer({
  models: {
    blogPost: Model.extend({
      author: belongsTo(),
      comments: hasMany(),
    }),

    author: Model,
    comment: Model
  },
})
import { createServer, Model, belongsTo, hasMany } from "miragejs"

createServer({
  models: {
    blogPost: Model.extend({
      author: belongsTo(),
      comments: hasMany(),
    }),

    author: Model,
    comment: Model
  },

  routes() {
    this.get("/posts", (schema) => schema.blogPosts.all());
  }
})
import { createServer, Model, belongsTo, hasMany } from "miragejs"

createServer({
  models: {
    blogPost: Model.extend({
      author: belongsTo(),
      comments: hasMany(),
    }),

    author: Model,
    comment: Model
  },

  routes() {
    // [postModel, postModel]
    // { attrs, fks, author, comments }
    this.get("/posts", (schema) => schema.blogPosts.all());
  }
})
import { createServer, Model, belongsTo, hasMany } from "miragejs"

createServer({
  models: {
    blogPost: Model.extend({
      author: belongsTo(),
      comments: hasMany(),
    }),

    author: Model,
    comment: Model
  },

  routes() {
    this.get("/posts", (schema) => schema.blogPosts.all());
    this.get("/posts/:id", (schema, request) => schema.blogPosts.find(request.params.id));
  }
})
import { createServer, Model, belongsTo, hasMany } from "miragejs"

createServer({
  models: {
    blogPost: Model.extend({
      author: belongsTo(),
      comments: hasMany(),
    }),

    author: Model,
    comment: Model
  },

  routes() {
    this.get("/posts", (schema) => schema.blogPosts.all());
    this.get("/posts/:id", (schema, request) => schema.blogPosts.find(request.params.id));
    this.post("/posts", (schema, request) => 
    	schema.blogPosts.create(JSON.parse(request.requestBody))
    );
  }
})
import { createServer, Model, belongsTo, hasMany } from "miragejs"

createServer({
  models: {
    blogPost: Model.extend({
      author: belongsTo(),
      comments: hasMany(),
    }),

    author: Model,
    comment: Model
  },

  routes() {
    this.get("/posts", (schema) => schema.blogPosts.all());
    this.get("/posts/:id", (schema, request) => schema.blogPosts.find(request.params.id));
    this.post("/posts", (schema, request) => 
    	schema.blogPosts.create(JSON.parse(request.requestBody))
    );
    this.delete("/posts/:id", (schema, request) => 
		schema.blogPosts.find(request.params.is).destroy()
    );
    // ... PUT, PATCH
  }
})
import { createServer, Model, belongsTo, hasMany } from "miragejs"

createServer({
  models: {
    blogPost: Model.extend({
      author: belongsTo(),
      comments: hasMany(),
    }),

    author: Model,
    comment: Model
  },

  routes() {
    this.resource("blogPosts", { path: "/posts" });
  }
})

Other key features of mirage.js

  • Factories / Seeds
  • Serializers

Demo Mode

Data "heavy" app

Bussines perspective

  • It's hard to show the product
  • Self-service

Why mirage.js?

  • Easy setup
  • Persistent data (db, relationships)
  • It can be used in tests
  • It can be used in development

Let's do it

REST API

but not that restful...

Custom serializers

this.get("/posts", (schema) => schema.posts.all()) // returns collection

this.get("/posts", (schema) => schema.db.posts.all()) // returns RAW data

Attributes builder

export default class ProtectModel extends Model {
    buildAttrs() {
        return new AttrsBuilder(this.attrs, this.fks);
    }
}

class AttrsBuilder {
    constructor(attrs, fks) {
        this.value = clone(attrs);
        this.fks = fks;
    }

    omit(keys) {}

    omitFks() {}

    pick(keys) {}

    mapId(name) {}

    addProperties(obj) {}

    mapPropertyName(targetKey, newKey) { }

    build() {
        return this.value;
    }
}
this.get("/folders/:sourceId/:parentId", (schema, { params: { parentId, sourceId } }) => {
  const folders = schema.folders.where({
    parentId: parentId,
    sourceId,
  });

  return folders.models.map((folder) =>
    folder.buildAttrs().pick(["id", "parentId", "path"]).build(),
  );
});
this.get("/folders/:sourceId/:parentId", ({ db }, { params: { parentId, sourceId } }) => {
  const folders = db.folders.where({
    parentId: parentId,
    sourceId,
  });

  return folders.map(({ id, parentId, path }) => ({ id, parentId, path }));
});

Performance

Β 

createServer({
  models: {
    folder: Model.extends({
      hasMany: files
    }),
    
    file: Model.extends({
      hasMany: versions
    }),
    
    version: Model.extends({
      hasMany: matches
    }),
    
    match: Model
  }
})

class FolderModel extends OurModel {
  isSensitve() {
    // check if any version of files under this folder
    // has some sensitive matches
    // πŸ’£
  }
}


// route
this.get("/sensitive-folders", (schema) => 
  schema.folders.all().models.filter(folder => folder.isSensitive())
)

Web Worker API

main thread

http request

worker

intercept only

data

mock

Persistence layer

Β 

πŸ€”

Where & When ?

  • LocalStorage?Β  - 5 MB ❌
  • IndexedDB - βœ…
  • onbeforeunload
  • sync event 😭 + async IndexedDb webWorker

Other lessons learned

  • relationship !== inheritance (polymorphic)
  • performance -> db vs schema (x10)
  • default ids are numbers

Pros of using mirage.js

  • single way for mocking backend
    (dev.env, demo.env, tests)
  • app prototyping
  • sharable app env config
    (bugs reproduction)

Cons

  • console.log vs network tab
  • lack of model validation
  • no db events (onChange etc.)

Summary

AKA - with mirage.js...

  • ... you can develop faster
  • ... you can prototype app without backend
  • ...you can write "better" tests

Thank you

Mirage.js as a demo.env

By Przemek Suchodolski

Mirage.js as a demo.env

  • 213