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.
.
├── 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!
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.
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.
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!
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
:
import { userFactory } from "./user"
export * from "./user"
export const factories = {
user: userFactory,
}
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.
import { Model } from "miragejs"
import { ModelDefinition } from "miragejs/-types"
import { User } from "../../types"
const UserModel: ModelDefinition<User> = Model.extend({})
export const models = {
user: UserModel,
}
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.
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:
import { routesForUsers } from "./user"
const endpoints = {
users: routesForUsers,
}
export { endpoints }
The config I’m using is very similar to what I described in my basic setup post.
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() })
}
Phew! Not so bad. And now I have type hinting and safety for all of my Mirage setup!
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!