Mock API in Parcel Project

When developing a frontend application, usually we create mocks for backend API, so that after the API contract is settled down, front and backend engineers can work independently. There are several ways to accomplish this task, such as start a dedicated server and let the build tool serve as a proxy, or we can add middleware directly into the build tool’s dev server, if applicable. Some tools can monkey patch the network calls to replace the response with mock data, and various unit testing tools provide their own way of mocking. In this article, I will focus on how to add middleware into Parcel‘s dev server to respond with mock data for API calls.

Parcel

API Proxy in Parcel’s development server

Parcel provides a dev server and supports API proxy out of the box. Under the hood, it uses connect and http-proxy-middleware to redirect API calls to a different server. It also provides the ability to customize the proxy behavior. For instance, by creating a file named .proxyrc.js in project’s root, we can manually redirect all API calls to a mock server listening on localhost:8080.

1
2
3
4
5
6
7
8
const { createProxyMiddleware } = require('http-proxy-middleware')

module.exports = function (app) {
const proxy = createProxyMiddleware('/api', {
target: 'http://localhost:8080/',
})
app.use(proxy)
}

In order to serve API calls directly in Parcel’s dev server, we just need to write our own middleware and wire it into the connect instance. Let’s name it mock-middleware, and it has the following functions:

  • Read source files from the /mock folder, and serve API calls with mock data.
  • When the files are updated, refresh the APIs as well.

Define mock files

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// /mock/user.js
const sendJson = require('send-data/json')

function login(req, res) {
const { username, password } = req.body
sendJson(req, res, {
id: 1,
nickname: 'Jerry',
})
}

module.exports = {
'POST /api/login': login,
}

Mock API are simple functions that accept standard Node.js request/response objects as parameters and receive and send data via them. The function is associated with a route string that will be used to match the incoming requests. To ease the processing of request and response data, we use body-parser to parse incoming JSON string into req.body object, and use send-data utility to send out JSON response, that helps setup the Content-Type header for us. Since body-parser is a middleware, we need to wire it into the connect app, before the mock-middleware we are about to implement.

1
2
3
4
5
6
7
8
// /.proxyrc.js
const bodyParser = require('body-parser')
const { createMockMiddleware } = require('./mock-middleware')

module.exports = function (app) {
app.use(bodyParser.json())
app.use(createMockMiddleware('./mock')) // TODO
}

Create router

To match the requests into different route functions, we use route.js.

1
2
3
4
5
6
7
8
9
10
11
12
13
// Create router and add rules.
const router = new Router()
router.addRoute('POST /api/login', login)

// Use it in a connect app middleware.
function middleware(req, res, next) {
const { pathname } = url.parse(req.url)
const m = router.match(req.method + ' ' + pathname)
if (m) m.fn(req, res, m.param)
else next()
}

app.use(middleware)

route.js supports parameters in URL path, but for query string we need to parse them on our own.

1
2
3
4
5
6
7
8
9
module.exports = {
// Access /user/get?id=1
'GET /:controller/:action': (req, res, params) => {
const { query } = url.parse(req.url)
// Prints { controller: 'user', action: 'get' } { id: '1' }
console.log(params, qs.parse(query))
res.end()
},
}

Combined with glob, we scan files under /mock folder and add all of them to the router.

1
2
3
4
5
6
glob.sync('./mock/**/*.js').forEach((file) => {
const routes = require(path.resolve(file))
Object.keys(routes).forEach((path) => {
router.addRoute(path, routes[path])
})
})

Watch and reload

The next feature we need to implement is watch file changes under /mock folder and reload them. The popular chokidar package does the watch for us, and to tell Node.js reload these files, we simply clear the require cache.

1
2
3
4
5
6
7
8
9
10
11
const watcher = chokidar.watch('./mock', { ignoreInitial: true })
const ptrn = new RegExp('[/\\\\]mock[/\\\\]')
watcher.on('all', () => {
Object.keys(require.cache)
.filter((id) => ptrn.test(id))
.forEach((id) => {
delete require.cache[id]
})

// Rebuild the router.
})

Now that we have all the pieces we need to create the mock-middleware, we wrap them into a class and provide a createMockMiddleware function for it. The structure is borrowed from HttpProxyMiddleware. Full code can be found on GitHub.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// /mock-middleware/index.js
class MockMiddleware {
constructor(mockPath) {
this.mockPath = mockPath
this.createRouter()
this.setupWatcher()
}

createRouter() {
this.router = new Router()
// ...
}

setupWatcher() {
watcher.on('all', () => {
// ...
this.createRouter()
})
}

middleware = (req, res, next) => {
// ...
}
}

function createMockMiddleware(mockPath) {
const { middleware } = new MockMiddleware(mockPath)
return middleware
}

module.exports = {
createMockMiddleware,
}

Standalone mock server

If you prefer setting up a dedicated server for mock API, either with Express.js or JSON Server, it is easy to integrate with Parcel. Let’s create a simple Express.js application first.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// /mock-server/index.js
const express = require('express')

const app = express()
app.use(express.json())

app.post('/api/login', (req, res) => {
const { username, password } = req.body
res.json({
id: 1,
nickname: 'Jerry',
})
})

const server = app.listen(8080, () => {
console.log('Mock server listening on port ' + server.address().port)
})

To start the server while watching the file changes, use nodemon.

1
2
yarn add -D nodemon
yarn nodemon --watch mock-server mock-server/index.js

Now configure Parcel to redirect API calls to the mock server.

1
2
3
4
5
6
// /.proxyrc.json
{
"/api": {
"target": "http://127.0.0.1:8080/"
}
}

Use concurrently to start up Parcel and mock server at the same time. In fact, it is more convenient to create a npm script for that. Add the following to package.json:

1
2
3
4
5
6
7
{
"scripts": {
"start": "concurrently yarn:dev yarn:mock",
"dev": "parcel",
"mock": "nodemon --watch mock-server mock-server/index.js",
}
}

References