Build a JSON Body-Parsing Middleware in Node.js
Nội Dung Chính
Build a JSON Body-Parsing Middleware in Node.js
🚀 The book Build Layered Microservices is out! Buy your own copy now at learnbackend.dev.
In Express, a middleware is a special type of function that allows to intercept incoming HTTP requests before they reach the controller. It can be used for a variety of things such as logging requests, verifying headers, parsing payloads, and so on.
In this article you’ll learn how to build a JSON body-parsing middleware that mimics the behaviour of the one provided by Express.
The Express JSON body-parsing middleware
The json()
built-in middleware function provided by Express parses incoming requests with JSON payloads. It adds a new body
object containing the parsed data on the request
object (i.e. req.body
), or an empty object ({}
) if there was no body to parse, the Content-Type
was not matched (i.e. application/json
), or an error occurred.
const { json } = require('express');
app.post('/', json(), (req, res) => {
console.log(req.body);
// ...
});
The middleware skeleton
Let’s start by creating a middleware function named parseJSON
that:
- Adds a new
body
property to the request object containing an empty object. - Calls the
next()
handler that forwards the request to the next component of the middleware stack.
function parseJSON(req, res, next) {
req.body = {};
next();
}
Verify the payload encoding
Let’s now verify the payload encoding by matching the value of the Content-Type
header contained in the headers
property of the request object against the JSON media type (also called MIME type) which is application/json
.
function parseJSON(req, res, next) {
req.body = {};
if (req.headers['content-type'] === 'application/json') {
// ...
}
next();
}
Gather the payload chunks
Since the content of an HTTP message can, in certain cases, be quite voluminous — like images or videos — is it usually broken down into several chunks of data that are sent one after the other.
To gather these chunks, we must set up two separate event listeners:
- One that will listen for a
data
event, and concatenate the received chunk to the previous one. - One that will listen for an
end
event signaling the end of the data stream.
function parseJSON(req, res, next) {
req.body = {};
if (req.headers['content-type'] === 'application/json') {
let data = '';
req.on('data', chunk => {
data += chunk;
});
req.on('end', () => {
// ...
});
}
next();
}
Parse the payload
Once all the raw data has been received, we need to parse it, which means converting it into a format the application can work with. In JavaScript, JSON strings can be converted into data objects using the built-in JSON.parse()
method.
function parseJSON(req, res, next) {
req.body = {};
if (req.headers['content-type'] === 'application/json') {
let data = '';
req.on('data', chunk => {
data += chunk;
});
req.on('end', () => {
req.body = JSON.parse(data);
});
}
next();
}
Change the function’s flow
Since event listeners are by nature asynchronous — due to their use of callback functions — we need to slightly change the flow of the middleware function to make sure the next()
handler is not invoked before all the data has been received.
For that, let’s:
- Move the existing call to
next()
within anelse
statement, so that it’s only invoked if theContent-Type
header doesn’t match the expected MIME type. - Add another call to
next()
in the callback function of the event listener in charge of handling theend
event, so that it’s only invoked once the data is parsed.
function parseJSON(req, res, next) {
req.body = {};
if (req.headers['content-type'] === 'application/json') {
let data = '';
req.on('data', chunk => {
data += chunk;
});
req.on('end', () => {
req.body = JSON.parse(data);
next();
});
} else {
next();
}
}
Handle parsing errors
In case of malformed data, the JSON.parse()
built-in will, by default, throw a SyntaxError
that will cause the application to crash if not handled properly. To solve this, we can add a try…catch
block that will catch the error and call the next()
handler.
function parseJSON(req, res, next) {
req.body = {};
if (req.headers['content-type'] === 'application/json') {
let data = '';
req.on('data', chunk => {
data += chunk;
});
req.on('end', () => {
try {
req.body = JSON.parse(data);
next();
} catch(error) {
next();
}
});
} else {
next();
}
}
Alternatively, we can avoid duplication and handle errors in a more elegant way by using a finally
statement, that will be invoked wether an error is thrown or not.
function parseJSON(req, res, next) {
req.body = {};
if (req.headers['content-type'] === 'application/json') {
let data = '';
req.on('data', chunk => {
data += chunk;
});
req.on('end', () => {
try {
req.body = JSON.parse(data);
} catch(error) {
// Ignore the error
} finally {
next();
}
});
} else {
next();
}
}
Test the middleware
Let’s start by exporting the parseJSON()
middleware function.
function parseJSON(req, res, next) {
// ...
}
module.exports = parseJSON;
And import it into a minimal Express application.
const express = require('express');
const parseJSON = require('./parseJSON');
const app = express();
app.post('/', parseJSON, (req, res) => {
console.log(req.body);
res.sendStatus(200);
});
app.listen(3000);
To verify that the middleware behaves as expected, we can now use cURL
to send:
A request with an invalid Content-Type
header; which should output an empty object (i.e. {}
).
curl -X POST -H 'Content-Type: text/plain' -d '{"name":"John"}' 127.0.0.1:3000
A request with an invalid payload; which should output an empty object (i.e. {}
).
curl -X POST -H 'Content-Type: application/json' -d 'name=John' 127.0.0.1:3000
A request with a valid Content-Type
header and a valid payload; which should output a populated object (i.e. { name: 'John' }
).
curl -X POST -H 'Content-Type: application/json' -d '{"name":"John"}' 127.0.0.1:3000
What’s next?
👉 You like this kind of content? Check out the book Build Layered Microservices at https://learnbackend.dev on how to build a production-ready layered authentication microservice using the Express framework, that lives up to the industry standards in terms of development practices and software architecture from the first line of code to the last line of documentation.