Published Sunday, September 8, 2024
MSW is a mocking library for API’s. What we’re gonna see is how to use msw for API mocking without necerrarily having it installed as a dependency.
For our project, msw
will be an optional dependency that developers will opt-in to install.
We’ll be using the tmdb API for this project. You’ll need an API key if you want to run the project without the mocks.
We’ll also be using Vite and styled-components.
Initializing the project
Installing and configuring msw
To install msw
locally without saving as dependency we run:
We need to first create a couple of types
. Inside src
, create a new directory called @types
.
Inside @types
, create two files:
msw.d.ts with the following:
browser.d.ts with the content:
declare module ' msw/browser '
You’ll need to edit tsconfig
to look for these types. Add this to it:
" typeRoots " : [ " ./node_modules/@types " , " ./src/@types/**/*.ts " ]
We need to update vite-env.d.ts
:
declare module ' msw/browser '
Now create a mocks
directory inside src
, and inside it create a msw
directory. Our first file here will be msw-utils
. The code is listed below:
export const isMswInstalled = async () : Promise < boolean > => {
This is simply going to check if msw
is installed.
The second file will be handlers
. Here’s the code:
import { isMswInstalled } from ' ./msw-utils '
import popularMovies from ' ../tmdb/popular-moviees.json '
export async function getHandlers () {
const isInstalled = await isMswInstalled ()
const { http, HttpResponse } = await import ( ' msw ' )
http. get ( ' /api/movie/popular?language=en-US&page=1 ' , () => {
const response = HttpResponse. json (popularMovies)
console. error ( ' Failed to import the package: ' , err)
console. log ( ' Package is not installed. ' )
Here’s the content for popular-movies.json
:
" backdrop_path " : " /yDHYTfA3R0jFYba16jBB1ef8oIt.jpg " ,
" genre_ids " : [ 28 , 35 , 878 ],
" original_language " : " en " ,
" original_title " : " Deadpool & Wolverine " ,
" overview " : " A listless Wade Wilson toils away in civilian life with his days as the morally flexible mercenary, Deadpool, behind him. But when his homeworld faces an existential threat, Wade must reluctantly suit-up again with an even more reluctant Wolverine. " ,
" poster_path " : " /8cdWjvZQUExUUTzyp4t6EDMubfO.jpg " ,
" release_date " : " 2024-07-24 " ,
" title " : " Deadpool & Wolverine " ,
" backdrop_path " : " /58D6ZAvOKxlHjyX9S8qNKSBE9Y.jpg " ,
" genre_ids " : [ 28 , 12 , 18 , 53 ],
" original_language " : " en " ,
" original_title " : " Twisters " ,
" overview " : " As storm season intensifies, the paths of former storm chaser Kate Carter and reckless social-media superstar Tyler Owens collide when terrifying phenomena never seen before are unleashed. The pair and their competing teams find themselves squarely in the paths of multiple storm systems converging over central Oklahoma in the fight of their lives. " ,
" poster_path " : " /pjnD08FlMAIXsfOLKQbvmO0f0MD.jpg " ,
" release_date " : " 2024-07-10 " ,
" backdrop_path " : " /stKGOm8UyhuLPR9sZLjs5AkmncA.jpg " ,
" genre_ids " : [ 16 , 10751 , 12 , 35 ],
" original_language " : " en " ,
" original_title " : " Inside Out 2 " ,
" overview " : " Teenager Riley's mind headquarters is undergoing a sudden demolition to make room for something entirely unexpected: new Emotions! Joy, Sadness, Anger, Fear and Disgust, who’ve long been running a successful operation by all accounts, aren’t sure how to feel when Anxiety shows up. And it looks like she’s not alone. " ,
" poster_path " : " /vpnVM9B6NMmQpWeZvzLvDESb2QY.jpg " ,
" release_date " : " 2024-06-11 " ,
" backdrop_path " : " /lgkPzcOSnTvjeMnuFzozRO5HHw1.jpg " ,
" genre_ids " : [ 16 , 10751 , 35 , 28 ],
" original_language " : " en " ,
" original_title " : " Despicable Me 4 " ,
" overview " : " Gru and Lucy and their girls—Margo, Edith and Agnes—welcome a new member to the Gru family, Gru Jr., who is intent on tormenting his dad. Gru also faces a new nemesis in Maxime Le Mal and his femme fatale girlfriend Valentina, forcing the family to go on the run. " ,
" poster_path " : " /wWba3TaojhK7NdycRhoQpsG0FaH.jpg " ,
" release_date " : " 2024-06-20 " ,
" title " : " Despicable Me 4 " ,
" backdrop_path " : " /3q01ACG0MWm0DekhvkPFCXyPZSu.jpg " ,
" genre_ids " : [ 28 , 80 , 53 , 35 ],
" original_language " : " en " ,
" original_title " : " Bad Boys: Ride or Die " ,
" overview " : " After their late former Captain is framed, Lowrey and Burnett try to clear his name, only to end up on the run themselves. " ,
" poster_path " : " /oGythE98MYleE6mZlGs5oBGkux1.jpg " ,
" release_date " : " 2024-06-05 " ,
" title " : " Bad Boys: Ride or Die " ,
This is going to mock the response for this url and will simply return the popularMovies
json file that we are loading here.
The last file is browser
. The content is as follows:
import { isMswInstalled } from ' ./msw-utils '
export const getWorker = async () => {
const isInstalled = await isMswInstalled ()
throw new Error ( ' MSW package is not installed. ' )
const mswBrowser = await import ( ' msw/browser ' )
const { setupWorker } = mswBrowser
const { getHandlers } = await import ( ' ./handlers ' )
const handlers = await getHandlers ()
if ( ! handlers || handlers.length === 0 ) {
throw new Error ( ' No valid handlers returned from getHandlers. ' )
return setupWorker ( ... handlers)
console. error ( ' Failed to import the package or setup worker: ' , err)
Creating the msw service worker
We need to create the msw service worker. Run the following to create it inside public:
This is the core of our mocking. It will check if msw
is installed and will throw an error if not. It then will setup the worker using our previously
created handlers.
Setting up main to enable mocks when mock is enabled
Now we need to setup main so that when a mock flag is set to true and it has msw
installed it starts the worker for us.
Inside main
:
const deferRender = async () => {
if ( import .meta.env. VITE_MOCK_ENABLED === ' true ' ) {
const { isMswInstalled } = await import ( ' ./mocks/msw/msw-utils.ts ' )
const hasMsw = await isMswInstalled ()
const { getWorker } = await import ( ' ./mocks/msw/browser.ts ' )
const worker = await getWorker ()
deferRender (). then (() => {
const rootElement = document. getElementById ( ' root ' )
createRoot (rootElement). render (
console. error ( ' Root element not found. ' )
We’re gonna need to set the flag VITE_MOCK_ENABLED
to true when we want the API mocking enabled.
To have access to this env
variable we need to change vite-env.d.ts
:
/// < reference types = " vite/types/importMeta.d.ts " />
/// < reference types = " vite/client " />
interface ImportMetaEnv {
readonly VITE_MOCK_ENABLED : string
readonly env : ImportMetaEnv
tsconfig
also needs to be updated to include vite-env.d.ts
. Here’s our tsconfig.json
:
" allowImportingTsExtensions " : true ,
" moduleResolution " : " Bundler " ,
" typeRoots " : [ " ./node_modules/@types " , " ./src/@types/**/*.ts " ],
" lib " : [ " dom " , " esnext " ],
Starting to code our application
Finally, we’ll start coding our application code. To install styled-components
type:
Creating the styles
Our first style will be movies-styled
inside src/comps/movie/movies-styled
:
import styled from ' styled-components '
export const StyledMovieList = styled. ul `
export const StyledMovieListItem = styled. li `
For the card
we’ll create a card-styled
in src/comps/card
:
import styled from ' styled-components '
import { BaseClassPrefix } from ' ../../constants/sc-constants '
export const StyledCard = styled.div. attrs ({
export const StyledCardCover = styled.div. attrs ({
className : `cl--card-cover` ,
export const StyledCardContent = styled.div. attrs ({
className : `cl--card-content` ,
Creating our views
Our first component will be a card
. Inside src/comps/card
:
import React from ' react '
import { StyledCard, StyledCardContent, StyledCardCover } from ' ./card-styled '
renderContent : () => React . ReactNode
const Card = ({ imageUrl , renderContent } : CardProps ) => {
< StyledCardContent >{ renderContent ()} </ StyledCardContent >
Then we’ll create movie-list-item
inside src/comps/movie
:
import React from ' react '
import Card from ' ../card/card '
interface MovieListItemProps {
const MovieListItem = ({ imageUrl , title , overview } : MovieListItemProps ) => {
const renderContent = () => {
< Card imageUrl = {imageUrl} renderContent = {renderContent} />
export default MovieListItem
Creating the movie hook
First, we’re gonna need to add a new env
variable inside vite-env.d.ts
:
interface ImportMetaEnv {
readonly VITE_TMDB_TOKEN : string
Then create the following hook inside src/hooks/movie.ts
:
const token = import .meta.env. VITE_TMDB_TOKEN
const bearer = ' Bearer ' + token
const getMovies = async () => {
const url = ` ${ baseUrl } /movie/popular?language=en-US&page=1`
const res = await fetch (url, {
' Content-Type ' : ' application/json ' ,
const data = await res. json ()
Creating the movie model
We need a model for the movie’s response, create a movie.ts
file inside src/models
:
original_language : string
Bringing all together
Finally, we’ll be able to connect everything in our movies
component. Inside src/comps/movies.ts
import React, { useEffect, useState } from ' react '
import { StyledMovieList } from ' ./movies-styled '
import MovieListItem from ' ./movie-list-item '
import useMovie from ' ../../hooks/movie '
import { Movie } from ' ../../models/movie '
const baseImagePath = `https://image.tmdb.org/t/p/w500`
const { getMovies } = useMovie ()
const [movies, setMovies] = useState < Movie []>([])
const fetchData = async () => {
const data = await getMovies ()
setMovies (data.results as Movie [])
{ movies ?. map (( movie , index ) => (
title = {movie?.original_title}
overview = {movie?.overview}
imageUrl = { ` ${ baseImagePath }${ movie?.poster_path } ` }
Now we include in the app
component:
import React from ' react '
import Movies from ' ./comps/movie/movies '
And lastly, we need to configure vite
so it can handle when msw is not installed and to proxy tmdb
requests.
Install plugin-alias
:
npm i @rollup/plugin-alias -D
To configure vite
, update vite.config.ts
with this:
import { defineConfig } from ' vite '
import react from ' @vitejs/plugin-react '
import alias from ' @rollup/plugin-alias '
async function resolveMswPath () {
return ' /empty-module.js '
export default defineConfig ( async () => {
const mswPath = await resolveMswPath ()
target : ' https://api.themoviedb.org/3 ' ,
rewrite : ( path : string ) => path. replace ( / ^ \/api / , '' ),
entries : [{ find : ' msw ' , replacement : mswPath }],
When using install with no-save
, it will need to install it every time you mess with the packages, so don’t forget to run msw install again.
Also, don’t forget to add an .env
file and set the env variables, like this:
VITE_TMDB_TOKEN=your-tmdb-token
Or set in package.json
.