Dec 08, 2022

Build a JSON Body-Parsing Middleware in Node.js

Build a JSON Body-Parsing Middleware in Node.js

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 an else statement, so that it's only invoked if the Content-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 the end 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

Related posts