miragejs: setup for typescript

2021-10-29

 | 

~5 min read

 | 

843 words

In MirageJS: Basic Setup In React, I outlined a straight forward Mirage setup for a React application. This post is about extending that concept into Typescript so that the server can provide (some) intellisense.

As my use of Mirage has expanded, I’ve wanted to use Models and Factories. However, I wasn’t satisfied with that alone. I also wanted to be able to use those factories in my routes.

I’ve found it helpful to have a structured approach to Mirage. I keep all of my Mirage files in a directory inside my src, alongside my code. This ensures that I can have access to the Mirage server when it comes time to testing too.

src
.
├── mirage
│   ├── endpoints
│   │   ├── index.ts
│   │   └── users.ts
│   ├── factories
│   │   ├── index.ts
│   │   └── users.ts
│   ├── models
│   │   ├── index.ts
│   │   └── users.ts
│   └── config.ts
│   └── index.ts
│   └── types.ts
└── App.tsx

This is likely overkill for a small server like this with just one endpoint, factory, and model. That said, I believe it scales well!

Let’s look at the pieces!

Types

One thing we need to be aware of when designing our solution is avoiding circular references.

By pulling the types file out into its own domain, I’m able to manage those more easily.

mirage/types.ts
import { Registry } from "miragejs"
import Schema from "miragejs/orm/schema"

import { models } from "./models"
import { factories } from "./factories"

type AppRegistry = Registry<typeof models, typeof factories>
export type AppSchema = Schema<AppRegistry>

While I wish I could reuse the AppRegistry, so far I have not yet found a way. The real value we’re getting here is the AppSchema which will come into play when we get to the endpoints.

Factories

Because I’m modeling my data with types, I want to be able to ensure that my factories are doing so properly. This means I would like to ensure that they’re typed!

mirage/factories/user.ts
import { Factory } from "miragejs"
import * as faker from "faker"
import { User } from "../../types"
export const userFactory = Factory.extend<User>({
  id(i) {
    return i
  },
  firstName() {
    return faker.name.firstName()
  },
  lastName() {
    return faker.name.lastName()
  },
})

By passing the type User into the generic for the Factory.extend method, I get type safety which ensures that I am building the same type that my app is expecting elsewhere.

Now, I can export these types in my index.ts:

mirage/factories/index.ts
import { userFactory } from "./user"
export * from "./user"

export const factories = {
  user: userFactory,
}

Models

Models works very similarly to Factories, except it’s even simpler because I’ve already defined my model as a type for my application.

As a result, I’ve opted to keep everything in a single index.ts file.

mirage/models/index.ts
import { Model } from "miragejs"
import { ModelDefinition } from "miragejs/-types"
import { User } from "../../types"

const UserModel: ModelDefinition<User> = Model.extend({})

export const models = {
  user: UserModel,
}

Endpoints

Almost all of this work was ultimately for the benefit of my endpoints. I prefer to keep my endpoints separate and then bring them into my server.

This is a little esoteric, but it helps to keep my server consturctor clean.

mirage/endpoints/user.ts
import { Response, Server } from "miragejs"
import { AppSchema } from "../types"

export function routesForUsers(server: Server) {
  server.get(`/users`, (schema: AppSchema, request) => {
    const users = schema.all("user")
    const seconds = new Date().getSeconds()
    return seconds % 17 === 0
      ? new Response(401, {}, { error: true })
      : new Response(200, {}, users)
  })
}

Here, we can see we’re using our AppSchema. This is what ensures that our schema provides type hinting on the all method.

Unfortunately, based on the guides, it looks like Mirage should support schema.users, however, I have not yet found a way to get the type support for that.

I bring all of my endpoints together in the index.ts file:

mirage/endpoints/index.ts
import { routesForUsers } from "./user"

const endpoints = {
  users: routesForUsers,
}

export { endpoints }

Config

The config I’m using is very similar to what I described in my basic setup post.

mirage/config.ts
import { createServer } from "miragejs"
import * as faker from "faker"
import { endpoints } from "./endpoints"
import { models } from "./models"
import { factories } from "./factories"

export function startMirage() {
  const server = createServer({
    models,
    factories,
    seeds(server) {
      server.createList("user", faker.datatype.number({ min: 2, max: 25 }))
    },
  })
  // logging
  server.logging = true

  // external URLs
  server.post(
    `${process.env.RAYGUN_URL}/:any`,
    () => new Promise((_res: any) => {}),
  )

  // internal URLs
  server.urlPrefix = process.env.API_URL ?? ""
  for (const namespace of Object.keys(endpoints)) {
    //@ts-ignore
    endpoints[namespace](server)
  }

  // Reset for everything else
  server.namespace = ""
  server.passthrough()
  // console.log({server})
  console.log({ dump: server.db.dump() })
}

Conclusion

Phew! Not so bad. And now I have type hinting and safety for all of my Mirage setup!

Acknowledgements

This approach was inspired by the solutions suggested in this Github issue and by my colleague Elliot Berger.



Hi there and thanks for reading! My name's Stephen. I live in Chicago with my wife, Kate, and dog, Finn. Want more? See about and get in touch!