Considering how loosely coupled web development is these days, leading to a separation of the frontend (mostly SPA) and backend(API driven) of our applications and often handled by different teams, one major thing to consider is the " blocked factor."
The blocked factor is how long a developer spends waiting on external API dependencies, hence preventing a feature development on the frontend or a project altogether.
Mocking is a way out of this blocked factor. They are easy to write, flexible, and stateless (hence testing scenarios repeatedly is easier) and ultimately provide a way out of external API dependencies.
Mocking allows us to simulate the backend API by specifying endpoints and the responses it gives.
The mocking framework
In this article, I would demonstrate how to use MSW (Mock Server Worker) to mock APIs for a todo react application.
Mock Service Worker is an API mocking library that uses Service Worker API to intercept actual requests. - mswjs.io
N.B: MSW is fully framework agnostic and also supports GraphQL. You should check it out!
Let's get started!
We need to install MSW.
$ npm install msw --save-dev
# or
$ yarn add msw --dev
Next would be to set up mocks. I do separate the API actions such as create, read, e.t.c from the mock servers themselves just for convenience.
Let’s do that.
[
{
"id": "BJSON65503",
"todo": "Write a new article",
"completed": false
},
{
"id": "BJSON44322",
"todo": "Work on side project",
"completed": true
}
]
A test sample of Todo Data. We could have used fakers also.
import todosData from './todoDB.json'
let todos: ITodo[] = [...todosData]
export interface ITodo {
id: string
todo: string
completed: boolean
}
export interface TodoUpdate {
todo?: string
completed?: boolean
}
export interface TodoUpdate {
todo?: string
completed?: boolean
}
async function create(todo: ITodo): Promise<ITodo> {
todos.push(todo)
return todo
}
async function readAll(): Promise<ITodo[]> {
return todos
}
async function read(todoId: string): Promise<ITodo | undefined> {
return todos.find(todo => todo.id === todoId)
}
async function update(
id: string,
update: TodoUpdate,
): Promise<ITodo | undefined> {
todos.forEach(todo => {
if (todo.id === id) {
return {...todo, update}
}
})
return await read(id)
}
async function deleteTodo(todoId: string): Promise<ITodo[]> {
return todos.filter(todo => todo.id !== todoId)
}
async function reset() {
todos = [...todosData]
}
export {create, read, deleteTodo, reset, update, readAll}
API actions
We can create our mock now.
If you are familiar with the express framework of node.js, the way to write the REST API Mock with MSW is similar.
import {setupWorker, rest} from 'msw'
import * as todosDB from '../data/todo'
interface TodoBody {
todo: todosDB.ITodo
}
interface TodoId {
todoId: string
}
interface TodoUpdate extends TodoId {
update: {
todo?: string
completed?: boolean
}
}
const apiUrl = 'https:todos'
export const worker = setupWorker(
rest.get<TodoId>(`${apiUrl}/todo`, async (req, res, ctx) => {
const {todoId} = req.body
const todo = await todosDB.read(todoId)
if (!todo) {
return res(
ctx.status(404),
ctx.json({status: 404, message: 'Book not found'}),
)
}
return res(ctx.json({todo}))
}),
rest.get(`${apiUrl}/todo/all`, async (req, res, ctx) => {
const todos = await todosDB.readAll()
return res(ctx.json(todos))
}),
rest.post<TodoBody>(`${apiUrl}/todo`, async (req, res, ctx) => {
const {todo} = req.body
const newTodo = await todosDB.create(todo)
return res(ctx.json({todo: newTodo}))
}),
rest.put<TodoUpdate>(`${apiUrl}/todo/update`, async (req, res, ctx) => {
const {todoId, update} = req.body
const newTodo = await todosDB.update(todoId, update)
return res(ctx.json({todo: newTodo}))
}),
rest.delete<TodoId>(`${apiUrl}/todo/delete`, async (req, res, ctx) => {
const {todoId} = req.body
const todos = await todosDB.deleteTodo(todoId)
return res(ctx.json({todos: todos}))
}),
)
Server worker used for client-side mocking with all the rest endpoints
Above, we have defined all the REST APIs with their responses, and if you notice, our REST endpoints all point to an HTTPS server (the apiUrl prefix). This is because service workers are to be served over HTTPS and not HTTP (Always note that).
We could attach the response status, JSON, e.t.c, which is great and similar to how our APIs behave normally.
The setupWorker or the handler has not yet started; hence the Service worker API will intercept no request.
We will start the worker in a development mode because we don’t want to hit a mock in production or even staging.
Let’s do that
if (process.env.NODE_ENV === 'development') {
const {worker} = require('./dev-server')
console.log(worker)
worker.start()
}
export {}
All we need to do is import the above file into the entry point of our application.
//index.ts
import './server'
Now, when we start our react application, we should see the following in the browser console.
Terrific!
If we make an API request to the “/todo/all” endpoint and look at the Network tab, we will see an actual request and the corresponding response served by the service worker API.
We will also get the todos from our todoDB.json as our response data.
This is great as we don’t have our backend ready and much more; we are not experiencing any blockage in our development process as frontend developers.
One of the major concerns regarding using mocks is maintenance, as the backend behavior might change rapidly, and we have to maintain the mocks. This is a valid point, but if we are to write tests (we will do in the second part of this article) for this APIs consumptions on the frontend, we would still need to maintain our mocks considering that our users won’t mock fetch or Axios hence our test shouldn’t as well, what if there is a way to share the handlers between the dev server and the test server hence maintaining just one handler and API actions.
We will explore the power of MSW much more in the next article.
Thank you for reading.
Top comments (3)
Thank you for the great overview, Fakorede!
Msw is a cool tech
Doesn't that just make frontend developers "full stack developers"?