Avoid Invalid Requests to Your Express.js Server Using celebrate

Sebastián Alvarez

In this article, we’ll build a REST API with Node.js and Express and protect one endpoint from arbitrary requests.

At first, we will develop an unsecured API, then we will add a own validation and analyze the shortcomings of it, finally we’ll use celebrate, a joi middleware for Express, to implement a completely flexible solution.

Prerequisites

Getting Started

In order to bring to life our API, we need to setup our new project:

$ mkdir swamp-api
$ cd swamp-api

Our Swamp API will allow us to register alligator nests and consult a list of them.

Inside our recently created directory, start an empty Node.js project:

$ npm init -y

As a last step we add to our project express and body-parser:

$ npm install express body-parser --save

Now we're ready to build the first version of our API!

A Minimal Rest API Without Any Validation

On this first section we’ll accomplish these steps:

  • Build the first API version with a GET /health endpoint
  • Test it using cURL
  • Add the GET /nest and POST /nest endpoints
  • Hit previous endpoints with arbitrary data

The next snippet presents a very basic server with a GET /health endpoint:

server.js

// Require express and initialize a new app from it
const express = require('express');
const app = express();

const PORT = 3000;

// Register a new GET endpoint, accessible through health route
// The response will be an json object with {ok: true} as content
app.get('/health', (req, res) => res.json({ok: true}));

// Start server on 3000 port
// Console.log will output the message when server is listening
app.listen(PORT, () => console.log(`Swamp API listening on port ${PORT}`));

Start it:

$ node server.js

The output after initialization should be Swamp API listening on port 3000.

On another terminal, try to access the GET /health endpoint:

$ curl -s http://localhost:3000

We should get {"ok": true} as response.

In order to get the API responses with a pretty format, I recommend a tool called jq. Simply pipe the previous command to it, like this: $ curl -s http://localhost:3000 | jq .


It’s time to add the core functionality: the /nest GET and POST endpoints. With these endpoints we’ll be able to retrieve the nests and add more.

server.js

const express = require('express');
const bodyParser = require('body-parser');
const app = express();

const PORT = 3000;
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({extended: false}));

let nests = [
  {
    momma: 'swamp_princess',
    eggs: 40,
    temperature: 31
  },
  {
    momma: 'mrs.chompchomp',
    eggs: 37,
    temperature: 32.5
  }
];

app.get('/health', (req, res) => res.json({ok: true}));
app.get('/nest', (req, res) => res.json(nests));
app.post('/nest', (req, res) => {
  const newNest = req.body;
  nests.push(newNest);
  return res.json(newNest);
});

First we require the body-parser module, next we register it on the Express app, after that we create a nests array to use them as a sample response. Lastly, we add two new endpoints (GET and POST):

  • GET retrieve as JSON the nests array
  • POST gets the body of the request, pushes it to the nests array and returns the new nest to the client

Everything is ready to try, we’ll start testing things out with the GET /nest endpoint:

$ curl -s http://localhost:3000/nest | jq .

The output should be a the list of nests we declared.

Now we can go with POST /nest:

$ curl -X POST \
 -H "Content-Type: application/json" \
 -d '{"momma": "lady.sharp.tooth", "eggs": 42, "temperature": 34}'\
 -s http://localhost:3000/nest | jq .

The output should now be the recently added nest.

Just for the sake of completeness we’ll hit GET /nest again:

$ curl -s http://localhost:3000/nest | jq .

An updated list of nests should be printed.

The first version of our service is completed, but if we care about data consistency we have a major problem on our hands.

We can add new nests trough POST /nest and the current implementation doesn’t validate that input, so we are accepting all bodies and pushing them to our list, like this:

$ curl -X POST \
 -H "Content-Type: application/json" \
 -d '{"eggs": 31.4, "temperature": "VERY HIGH"}'\
 -s http://localhost:3000/nest | jq .
$ curl -s http://localhost:3000/nest | jq .

The invalid nest now is part of the list.

A Custom Solution

Based on the previous server implementation, we’ll follow these steps:

  • Update the POST /nest endpoint to apply basic validation for the request body
  • Test with incorrect information

Here is the new version of POST /nest endpoint, we’ll update only the POST /nest endpoint:

server.js

app.post('/nest', (req, res) => {
  const newNest = req.body;
  const { momma, eggs, temperature } = newNest;

  if (!momma || !eggs) {
    return res.status(400).json({error: 'Not valid nest'});
  }

  nests.push(newNest);
  return res.json(newNest);
  
});

Here we’re not implementing a complete solution, this is just an example to illustrate how difficult it could be to come up with a validation that cares about more than one scenario.

We should be focused on what our API is needed for not put our efforts on how and when to validate input information.

And that’s where celebrate enters and the next section is all about it and how to implement it.

Using celebrate for Schema-Based Validation

celebrate allows us implement a flexible validation based on a predefined schema.

For this purpose, a schema is a JavaScript object that describes how requests (URL parameters, bodies, headers, etc.) must be, an example:

const schema = {
  body: {
    name: Joi.string().required(),
    age: Joi.number().integer().required()
  }
};

This schema defines (using joi, library that celebrate wraps) that a valid body is only one that has name as string and age as integer, anything outside of this will trigger an error.

Note that the schema is an object that's inside another object called body, this name is required as it is in order to check the body object on the request object.


With this information, we define here the steps to implement celebrate:

  • Install the celebrate module
  • Define a schema for the POST /nest endpoint
  • Add a new validation middleware, that implements our schema
  • Implement a error handler that will be called for us if the celebrate validation fails
$ npm install --save celebrate

Here is the updated version, this time using the celebrate middleware:

server.js

const express = require('express');
const bodyParser = require('body-parser');
const { celebrate, Joi } = require('celebrate');

const app = express();

const PORT = 3000;
let nests = [
  {
    momma: 'swamp_princess',
    eggs: 40,
    temperature: 31
  },
  {
    momma: 'mrs.chompchomp',
    eggs: 37,
    temperature: 32.5
  }
];

const nestSchema = {
  body: {
    momma: Joi.string().required(),
    eggs: Joi.number().integer(),
    temperature: Joi.number()
  }
};

app.use(bodyParser.json());
app.use(bodyParser.urlencoded({extended: false}));

app.get('/health', (req, res) => res.json({ok: true}));
app.get('/nest', (req, res) => res.json(nests));
 app.post('/nest', celebrate(nestSchema), (req, res) => {
  const newNest = req.body;
  nests.push(newNest);
  return res.json(newNest);
});

 app.use((error, req, res, next) => {
  if (error.joi) {
    return res.status(400).json({error: error.joi.message});
  }

  return res.status(500).send(error)
});

app.listen(PORT, () => console.log(`Swamp API listening on port ${PORT}`));

We start by requiring the celebrate and Joi from the celebrate module.

After that we define a new schema called nestSchema that has momma and eggs as required values with defined types (a string and an integer, respectively), in addition to this we add a temperature field, as a number, so it will accept an integer or a float.

The next update is for the POST /nest endpoint, we can see that before pushing our new nest we are calling the celebrate function with the schema as argument, and internally celebrate will handle the validation for us.

The last update is the addition of a error handler middleware, because celebrate is in charge of everything it needs a way to catch the possible errors, these errors will have a joi property as true so we can detect them.

We’re ready to test it again, first with an invalid body on POST /nest:

$ curl -X POST \
 -H "Content-Type: application/json" \
 -d '{"eggs": 31.4, "temperature": "VERY HIGH"}'\
 -s http://localhost:3000/nest | jq .

The message we should receive will look like this:

{
  "error": "child \"momma\" fails because [\"momma\" is required]"
}

Remember on the error handler that we check if it has the joi property as true? The error message that we have here is the one that celebrate prepared for us. Now we know that momma field was required, and we can update our request.

$ curl -X POST \
 -H "Content-Type: application/json" \
 -d '{"momma": "msg.alligator", "eggs": 31.4, "temperature": 33}'\
 -s http://localhost:3000/nest | jq .
{
  "error": "child \"eggs\" fails because [\"eggs\" must be an integer]"
}

Now celebrate is alerting us that the eggs field must be an integer, so we will update the request again.

$ curl -X POST \
 -H "Content-Type: application/json" \
 -d '{"momma": "Mrs Alligator", "eggs": 31, "temperature": 33}'\
 -s http://localhost:3000/nest | jq .
{
  "momma": "Mrs Alligator",
  "eggs": 31,
  "temperature": 33
}

Finally, we have as result the nest we pushed, that means that the request was valid and will be pushed to the nests list, we can verify with a request to GET /nest:

$ curl -s http://localhost:3000/nest | jq .

You've made it!, the Swamp API will have consistent information.

Conclusion

The code presented here has a lot of room for improvement.

I highly encourage you to follow the official documentation and enhance this server, limiting the minimum and maximum for eggs and temperature could be good next steps!

  Tweet It

🕵 Search Results

🔎 Searching...

Sponsored by #native_company# — Learn More
#native_title# #native_desc#
#native_cta#