DEV Community πŸ‘©β€πŸ’»πŸ‘¨β€πŸ’»

Cover image for Announcing "@mswjs/data"β€”data modeling library for testing JavaScript applications
Artem Zakharchenko
Artem Zakharchenko

Posted on • Updated on

Announcing "@mswjs/data"β€”data modeling library for testing JavaScript applications

Introduction

It's been more than a year since Mock Service Worker (MSW) began to appear in people's package.json, improving the way you write tests, iterate on features, and debug API-related issues. We are incredibly thankful for everybody who supported us and gave the library a chance in their projects. That allowed us to gather a ton of feedback based on the usage scenarios you face every day. It is with that feedback that we can move the project forward to ease your testing and development workflow even more. And it is with that feedback that we are able to make this announcement.

MSW was deliberately designed with only the essentials of API mocking in mind: interception of requests and response mocking. A huge focus was made on leveraging Service Worker API to enable a one-of-a-kind experience and support the same request handlers across different environments. While some alternative libraries come with built-in assertions or data-modeling options, our team has chosen a horizontal way to scale the project: distribute complimentary, on-demand libraries instead of stuffing dozens of functions and methods into a single package.

Some of the most popular questions developers have when starting with MSW are related to data. You immediately notice how the library is agnostic to how you create and update data in your handlers. You may use a plain array or Map to manage resources. Perhaps, you may even design a custom database abstraction that manages those resources in a more standardized way.

No matter what data-related setup you end up with, it's there to answer the following questions:

  • How to describe data resources?
  • How to implement CRUD operations on data?
  • How to persist the changes made to the data?

Today we are proud to announce the Data libraryβ€”a standalone package to model and query data in your tests and beyond. Although the project is at the early stage of development and doesn't solve every problem right away, letting you try it out and gather your feedback is crucial for us to refine and shape the future experience.

GitHub logo mswjs / data

Data modeling and relation library for testing JavaScript applications.

Without further a due, let's talk about what's to become the recommended way to work with data in Mock Service Worker.


Data modeling

First, let's get acquainted with the two main terms that the Data library operates with:

  • Modelβ€”description of the data. Think of it as a blueprint that describes what properties the data has.
  • Entityβ€”an instance of a particular model. This is the exact data that implements its model description.

When working with Data, you define models and relationships between them, which, effectively, result in a virtual database being created.

Install the package into your project:

npm install @mswjs/data 
Enter fullscreen mode Exit fullscreen mode

Now, let's create a new "user" model:

import { factory, primaryKey } from '@mswjs/data'

const db = factory({
  user: {
    id: primaryKey(Math.random),
    firstName: () => 'John',
    age: () => 18,
  },
})
Enter fullscreen mode Exit fullscreen mode

Models are defined by calling the factory function and providing it with the object where keys represent model names, and values stand for model definitions. Each property in the model definition has an initializerβ€”a function that seeds a value and infers its type.

Notice how the id property equals primaryKey. Each model must have a primary key, which acts as a unique ID in a conventional database table. Data exposes the primaryKey function that you should use to mark a certain property as the model's primary key.

In the example above, we're using plain functions that return static data. This means that each time a user is created, it will have firstName: "John" and age: 18. While this is a good foundation to build upon, the static nature of values may limit your data scenarios. Consider using tools like Faker to define models with randomly generated realistic data.

import { factory, primaryKey } from '@mswjs/data'
import { name, random } from 'faker'

const db = factory({
  // Create a user model with a random UUID and first name.
  user: {
    id: primaryKey(random.uuid),
    firstName: name.findName,
    age: random.number,
  },
})
Enter fullscreen mode Exit fullscreen mode

You can define multiple models and relationships between them within the same factory:

import { factory, primaryKey, oneOf } from '@mswjs/data'
import { random } from 'faker'

const db = factory({
  book: {
    isbn: primaryKey(random.uuid),
    // "book.publisher" is a relational property
    // that references an entity of the "publisher" model.
    publisher: oneOf('publisher')
  },
  publisher: {
    id: primaryKey(random.uuid),
    name: random.words,
  }
})
Enter fullscreen mode Exit fullscreen mode

Learn more about defining model relationships.


Seeding

Once the models are defined, create an entity of a particular model by calling the .create() method on it:

db.user.create()
Enter fullscreen mode Exit fullscreen mode

Creating an entity without any arguments will fill its properties using the value initializers you've specified in the model definition.

The .create() method accepts an optional argument that stands for the initial values of the entity. For example, if we wish to create a user with a fixed "firstName" value, we can provide that value in the initial values object:

db.user.create({
  // Uses an exact value for the "firstName" property,
  // while seeding the "id" based on its getter.
  firstName: 'John',
})
Enter fullscreen mode Exit fullscreen mode

Querying client

Apart from the data modeling functionality, this library provides a querying client that allows you to find, modify, and delete entities on runtime. The querying client brings Data to life, as it enables dynamic scenarios against the generated data.

Each model supports the following querying methods:

  • .findFirst(), finds the first entity that matches a query.
  • .findMany(), finds multiple entities that match a query.
  • .count(), returns the number of entities for a model.
  • .update(), updates an entity that matches a query.
  • .updateMany(), updates multiple entities that match a query.
  • .delete(), deletes an entity that matches a query.
  • .deleteMany(), deletes multiple entities that match a query.

The most basic example of querying is finding an entity by its primary key. In our case, we can find a user by its "id" like this:

// Find a user with the given "id" (primary key).
const user = db.user.findFirst({
  where: {
    id: {
      equals: 'abc-123',
    },
  },
})
Enter fullscreen mode Exit fullscreen mode

Let's focus on where part of the query above. When querying entities, where is a predicate object that describes the criteria against an entity. The structure of that predicate is the following:

{
  where: {
    [property]: {
      [operator]: expectedValue
    }
  }
}
Enter fullscreen mode Exit fullscreen mode
  • [property], a known property of the model. In the case of our "user" model, this can be "id" or "firstName".
  • [operator], an operator function name that compares the actual and expected values of the referenced property.

Operators depend on the value type that's being queried. When querying a string (like we do with where.id, where "id" is of the String type), operators like equals, notEquals, contains, in, and others are available. When querying a number, you have access to the gt, lte, between, etc. operators instead.

Querying methods are strongly typed, validating the known model properties and value-based operators on build time. Experiment with your models to learn about all the different options at your disposal!

In a similar fashion, we can query multiple entities. Here's how we can get all the users that satisfy a certain age criteria:

// Returns all users whose "user.age"
// is greater or equal 18.
const users = db.user.findMany({
  where: {
    age: {
      gte: 18,
    },
  },
})
Enter fullscreen mode Exit fullscreen mode

Data supports cursor and offset pagination to work with large data sets.

There is much more functionality that Data provides, so don't hesitate to explore the library API. Refer to the documentation for API definition and usage examples.


Integration with Mock Service Worker

Here's a gigantic cherry on top: you can turn any data model into request handlers (both REST and GraphQL) to encapsulate its operations, like creation and querying, under the respective API routes.

Generating REST API

Using our db and its "user" model, we can turn it into a REST API "server" in a single command:

// src/mocks/browser.js
import { setupWorker } from 'msw'
// Import the "db" object.
import { db } from './db'

const worker = setupWorker(
  // Generate REST API request handlers
  // based on the "user" model.
  ...db.user.toHandlers('rest')
)

worker.start()
Enter fullscreen mode Exit fullscreen mode

Looks unfamiliar? Learn how to get started with Mock Service Worker.

Calling .toHandlers() on a model generates CRUD routes for working with that model. In the example above, the following request handlers will be created:

  • GET /users/, returns all users in the database.
  • GET /users/:id, returns a user by their primary key (id).
  • POST /users, creates a new user.
  • PUT /users/:id, updates an existing user.
  • DELETE /users/:id, deletes a user by their primary key.

Notice how the model name is pluralized ("user*s*") to reflect the proper semantics when working with the "user" resource.

With the handlers established, you can create and query users in your application as you would do against an actual HTTP server:

// Create a new user in the database.
fetch('/users', {
  method: 'POST',
  // The body is used as the initial entity values.
  body: JSON.stringify({
    id: 'abc-123',
    firstName: 'John',
  }),
})

// Then, query the created user.
fetch('/users/abc-123')
Enter fullscreen mode Exit fullscreen mode

Explore this interactive sandbox to learn more about generating REST API handlers from your data models:

Generating GraphQL API

A model can also generate GraphQL handlers:

const worker = setupWorker(...db.user.toHandlers('graphql'))
Enter fullscreen mode Exit fullscreen mode

This command generates the following GraphQL schema with its types based on your model:

type Query {
  user(where: UserQuery): User
  users(where: UserQuery, take: Int, skip: Int, cursor: ID): [User!]
}

type Mutation {
  createUser(data: UserInput!): User!
  updateUser(where: UserQueryInput!, data: UserInput!): User!
  updateUsers(where: UserQueryInput!, data: UserInput!): [User!]
  deleteUser(where: UserQueryInput!, data: UserInput!): User
  deleteUsers(where: UserQueryInput!, data: UserInput!): [User!]
}
Enter fullscreen mode Exit fullscreen mode

All the generated GraphQL types and their names are based on your model name and properties.

With your model turned into request handlers, you can query its entities as you would usually do in GraphQL. Here's an example that uses Apollo to get a user entity by ID:

import { useQuery, gql } from '@apollo/client'

const GET_USER = gql`
  query GetUser {
    # Hey, it's the same query as in the ".findMany()" method!
    user(where: { id: { equals: "abc-123" } }) {
      firstName
      age
    }
  }
`

useQuery(GET_USER)
Enter fullscreen mode Exit fullscreen mode

Explore the GraphQL example on Codesandbox:


Call for Contributors!

Data is a new library that has a long way to go and multiple areas to improve. For instance, these are some of the features we'd love to implement:

  • Client-side persistence;
  • Server-side rendering support;
  • .createMany() method to seed multiple entities at once, respecting their relations.

If you would like to learn about data modeling or find this area fascinating, join as a contributor and shape the way developers would model their fixtures.

You can also support the project financially by sponsoring it on Open Collective, allowing the team to work on bug fixes and stunning new features. Your support will not go unnoticed!


Resources & links

GitHub logo mswjs / data

Data modeling and relation library for testing JavaScript applications.

Top comments (2)

Collapse
pke profile image
Philipp Kursawe

What I still don't get about MSW: Is it supposed for testing or production code?

Collapse
kettanaito profile image
Artem Zakharchenko Author • Edited on

Hey, Philipp.
MSW is a development tool. You can use it for testing, prototyping, debugging, anything concerning API, really. You are unlikely to need it in production where you have actual servers. Nothing stops you from shipping it, though, and there are some educational websites that use MSW as a server replacement for simplicity's sake.

🌚 Friends don't let friends browse without dark mode.

Sorry, it's true.