Disclaimer: I am not a native speaker, so please forgive me for a large number of mistakes on the site

API mocking with MSW library

2022-12-09 #vue #javascript

Table of contents

Ofthen when you’re working on a frontend part of your application, there are no implemented backend API yet, and you have to mock it. In this article I’ll tell how you can use the MSW library for it and why you should give it a try.

You can find all code in a Github repository.

Creating our demo

Our demo project is going to be a simple web application in vanilla javascript. We will also use Axios package for https requests and Vite as a build tool.

At first, we need a skeleton for our application. Luckily, vite can generate if for us. Go to the directory where you plan to create a repository and run this command:

npm create vite@latest mswdemo -- --template vanilla

The --template vanilla flag tells vite to create template project for vanilla javascript. Then, go to the mswdemo directory and install all required packages:

npm install

Now we can run our dev server and see what we’ve got:

npm run dev

We will create a service for displaying information about current user. To get user data, we have to make an HTTP request to a server, but our backend part is not implemented yet, so we will mock it. All what we need for start is to know how our API will work - to which URL we need to make request,which parameters it will expect and what kind of response it will return.

API

Here is the description of our API:

GET /user/:id - Returns our user data

User data is a JSON file with fields:

id
firstName
lastName
email
phone
registeredAt

Creating app’s main page

This topic is not about good design, so we’ll create very simple main page with the user information.

Vite’s entry point is the index.html file. By default, html is inserted into this file via main.js file.

We don’t need any javascript interactions for now, so let’s comment the line where this file is included and create the html structure for our page:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/vite.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Vite App</title>
  </head>
  <body>
    <div id="app"></div>
    <!-- <script type="module" src="/main.js"></script> -->

    <h2> User information: </h2>
    <ul>
        <li id="firstName"> First name </li>
        <li id="lastName"> Last name </li>
        <li id="email"> Email </li>
        <li id="registered"> Registered at </li>
        <li id="login"> Login </li>
    </ul>
  </body>
</html>

If we run npm run dev command, our page will look like this:

Now we want to fill this page with actual data. For the first time, we’re going to use one of the many online services - later you’ll see that MSW is better, but you should try both ways to see the difference.

First, create the api directory. Inside this directory, create a userService.js file:

export const getUserInfo = async () => {
    try {
        const response = await fetch("https://jsonplaceholder.typicode.com/users/1")
        const info = await response.json()
        return info
    } catch(e) {
        console.error("Error in getUserInfo: ", e)
        return null
    }
}

This file contains only one function - getUserInfo. It fetches info about one single user.

It’s also a good idea to let the user know that data is being loaded. We will hide information block while data is loading and show to the user another block with “Loading” text in it. When fetch is finished, we will hide the “loading” block and show the “User info” block.

Here’s the source of index.html:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/vite.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Vite App</title>
  </head>
  <body>
    <div id="app"></div>
    <script type="module" src="/main.js"></script>

    <div id="userInfo">
        <h2> User Information: </h2>
        <ul>
            <li id="firstName"> First name </li>
            <li id="lastName"> Last name </li>
            <li id="email"> Email </li>
            <li id="registered"> Registered at </li>
            <li id="login"> Login </li>
        </ul>
    </div>
    <div id="loading">
        Loading, please wait...
    </div>
  </body>
</html>

Let’s add our logic. It’s contained in the main.js:

import { getUserInfo } from "./api/userService"



const loadInfo = async () => {
    // Hide user info block
    document.getElementById('userInfo').hidden = true;

    // show "loading" block
    document.getElementById('loading').hidden = false;

    // Fetch user info
    const info = await getUserInfo()

    // Fill fields with user's data
    if (info) {
        document.getElementById('firstName').innerHTML = info.name
        document.getElementById('email').innerHTML = info.email
        document.getElementById('login').innerHTML = info.login

        // Hide loading state and show user info
        document.getElementById('userInfo').hidden = false
        document.getElementById('loading').hidden = true
    }
}

// Fetch user info
loadInfo()

Our page looks like this now:

Not all fields filled with data, because fake API service doesn’t provide all what we need. And that’s one of the downsides of online services.

Some refactoring

Our getUserinfo can be better:

For the base url, we are going to use .env file. Create it in the project’s root directory:

VITE_BASE_URL="https://jsonplaceholder.typicode.com"

And here is a modified version of getUserInfo:

export const getUserInfo = async (id) => {
    try {
        const baseUrl = import.meta.env.VITE_BASE_URL
        const response = await fetch(baseUrl + `/users/${id}`)
        const info = await response.json()
        return info
    } catch(e) {
        console.error("Error in getUserInfo: ", e)
        return null
    }
}

And after that, we have to pass user’s id to this function in the main.js file:

....
// Fetch user info
const info = await getUserInfo(2)
...

Now, I think it’s enough for our test project. Here, we have some problems:

MSW framework

MSW is a library for mocking API responses. It can handle REAL https calls and return any data you want for them. It means that if your api isn’t implemented yet, or even if you have no access to the internet, you can use it like if your app is talking to the real backend server.

First of all, let’s install it:

npm i msw --save-dev

Then, create mocks directory and create a handlers.js file inside it. This file will contain handlers for our https calls that return mock data:

// src/mocks/handlers.js
import { rest } from 'msw'
export const handlers = [
  // Handles a POST /login request
  rest.post('/login', null),
  // Handles a GET /user request
  rest.get('/user', null),
]

So, basically, all we need to do is to return mock data through these handlers. When our application sends HTTP requests, our handler catches them. Then, it processes all matched requests and responds with mocked data. All unprocessed requests are sent further. Let’s write our handler for the /user/:id request:

// src/mocks/handlers.js
import { rest } from 'msw'

export const handlers = [
    rest.get('https://mydomain.com/users/:id', (req, res, ctx) => {
        return res(
            ctx.status(200),
            ctx.json({
                firstName: "John",
                lastName: "Doe",
                registered: "2022-02-01",
                login: "john_doe_123",
                email: "johndoe@mailcomain.com"
            }),
        )
    }),
]

As was told before, the nice thing here is that we can send responses with the structure that we exactly need.

Now, we can change the VITE_BASE_URL variable in our .env file to https://mydomain.com (It may be anything, actually, you can put here your real url).

The last thing we need to do is to register a Service Worker for requests interception. The MSW library can create it for us. Run this command:

npx msw init public --save

Then, create a /mocks/browser.js file:

// src/mocks/browser.js
import { setupWorker } from 'msw'
import { handlers } from './handlers'
// This configures a Service Worker with the given request handlers.
export const worker = setupWorker(...handlers)

This file setups service workers with our route handlers from the handlers.js file.

Now, open main.js file and import our worker from the browser.js module:

import { worker } from "./mocks/browser"

Then, add following code:

// Start service workers
if (process.env.NODE_ENV === "development") {
    worker.start();
}

Important notice - we should run our worker only in development mode, because we don’t want it to intercept our request in production.

Finally, since now we have full data structure that we need, modify loadInfo function:

const loadInfo = async () => {
    // Hide user info block
    document.getElementById('userInfo').hidden = true;

    // show "loading" block
    document.getElementById('loading').hidden = false;

    // Fetch user info
    const info = await getUserInfo(2)

    // Fill fields with user's data
    if (info) {
        document.getElementById('firstName').innerHTML = info.firstName
        document.getElementById('lastName').innerHTML = info.lastName


        document.getElementById('email').innerHTML = info.email
        document.getElementById('login').innerHTML = info.login

        document.getElementById('registered').innerHTML = info.registered

        // Hide loading state and show user info
        document.getElementById('userInfo').hidden = false
        document.getElementById('loading').hidden = true
    }
}

That’s all. Run npm run dev and open console (F12 key). We should see the message:

[MSW] Mocking enabled.

As you can see, our page now shows user data from our mock handler:

One more thing - we haven’t took into account user id that we pass to getUserInfo. With msw, it’s easy to get access to route parameters:

//src/mocks/handlers.js
import { rest } from 'msw'

function User(id) {
    this.firstName = "John_" + id
    this.lastName = "Doe_" + id
    this.registered = "2022-02-01"
    this.login = "john_doe_" + id
    this.email = this.login + "@maildomain.com"
}

export const handlers = [
    rest.get('https://mydomain.com/users/:id', (req, res, ctx) => {

        const { id } = req.params // Get user id

        return res(
            ctx.status(200),
            ctx.json(
                // And generate different mock data for different users
                new User(id)
            ),
        )
    }),
]