DEV Community

Cover image for How to use celebrate with Node.js
Matt Angelosanto for LogRocket

Posted on • Originally published at blog.logrocket.com

How to use celebrate with Node.js

Written by Hulya Karakaya✏️

Imagine you have created a note-taking app with a login system where users can create an account and add their notes. Users need to type their email and name to sign in. Your job as a developer is to ensure the data you get from the user is the data you’re looking for, and it’s in the correct format, before persisting them in a database.

Validating user input sent from user requests is highly important for a couple of reasons:

  • Helps mitigate the attack surface
  • Protects from attacks like DDOS, cross-site scripting, command injection and SQL injection
  • Ensures data consistency
  • Helps identify and filter malicious data

This type of validation is called server-side validation, and it’s a critical part of developing applications. Luckily, there are several libraries that take care of this task for us.

Two of the best libraries for this are joi and celebrate. Joi is an object schema description language and validator for JavaScript objects. In this article, we’ll look at how to use these libraries and the benefits they provide for frontend dev.

By the end of this tutorial, you’ll be able to validate incoming user inputs coming from req.body, validate req.headers, req.params, req.query, and req.cookies, and handle errors.

We'll demo some API routes for the note-taking app that requires user input and validates it.

If you want to see the complete project developed throughout this article, take a look at the GitHub project. Feel free to clone it, fork it, or submit an issue.

Contents

What are joi and celebrate?

Joi is a standalone validation module that can be used alongside celebrate. Joi describes the client request within a schema. A schema is a JavaScript object that describes how client requests like parameters, request body, and headers must be formatted. They are made of a type and a succession of rules, with or without parameters.

Celebrate uses this schema to implement flexible validation middleware. It takes a schema and returns a function that takes the request and a value. If the value is valid, celebrate will call the next middleware in the chain. If the value is invalid, celebrate will call the error handler middleware.

You can validate req.params, req.headers, req.body, req.query, req.cookies and req.signedCookies before any handler function is called. We'll go into detail about how to validate these later in this article.

Getting started with a sample Node.js app

Begin by opening up your terminal and navigating to the directory where you want to place your project:

mkdir notes && cd notes
Enter fullscreen mode Exit fullscreen mode

Create a new Node project by running:

npm init -y 
Enter fullscreen mode Exit fullscreen mode

This will generate a package.json file in the root of your project. The --yes or -y flag will answer "yes" to all questions when setting up package.json.

Now, install the required dependencies by running:

npm install express body-parser cookie-parser
npm install nodemon -D
Enter fullscreen mode Exit fullscreen mode

Let’s review our installed packages:

  • Express is one of the most popular web framework for Node. It's used for creating web servers and APIs
  • body-parser is a middleware that parses the body of incoming requests, and exposes the resulting object on req.body
  • cookie-parser parses the cookies of incoming requests, and exposes the resulting object on req.cookies
  • Nodemon is used for automatically restarting the server when we make changes to our code.

The npm init command assigns index.js as the entry point of our application. Go ahead and create this file at the root of your project:

touch index.js
Enter fullscreen mode Exit fullscreen mode

Next, open up your favorite code editor, and create the boilerplate code for instantiating Express and setting up the server:

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

// parse application/json
app.use(bodyParser.json());

const PORT = process.env.PORT || 4001;

app.listen(PORT, () => {
  console.log(`Server is listening on port ${PORT}`);
});
Enter fullscreen mode Exit fullscreen mode

Here, we've imported Express and BodyParser and invoked the Express function to create our server. The server will listen on port 3000.

Running the app

Go to your package.json file and add a script to run our server with nodemon:

"scripts": {
    "start": "nodemon index.js"
  }
Enter fullscreen mode Exit fullscreen mode

Now, we can run our server from terminal by running npm start. This will start nodemon and watch for changes in our code.

Creating routes

Now that our application is listening for requests, we can create some routes:

  • POST /signup for creating a new user account
  • GET /notes for retrieving the notes
  • DELETE /notes/:noteId for deleting a note

Next, we'll look at how to validate the request data via joi and celebrate.

Installing joi and celebrate for schema-based validation

We can install joi and celebrate via npm like so:

npm install joi celebrate
Enter fullscreen mode Exit fullscreen mode

Joi allows you to describe data in an intuitive, readable way via a schema:

{
  body: Joi.object().keys({
    name: Joi.string().alphanum().min(2).max(30).required(),
    email: Joi.string().required().email(),
    password: Joi.string().pattern(new RegExp('^[a-zA-Z0-9]{3,30}/pre>)).required().min(8),
    repeat_password: Joi.ref('password'),
    age: Joi.number().integer().required().min(18),
    about: Joi.string().min(2).max(30),
  })
}
Enter fullscreen mode Exit fullscreen mode

According to this schema, a valid body must be an object with the following keys:

  • name, a required string with at least two characters and up to 25 characters (alphanumeric characters only)
  • email, a required string in an email format
  • password, a required string with at least eight characters, which should match the custom regex pattern
  • repeat_password, which should match the password
  • age, a required number with an integer value of 18 or more
  • about, a string with at least two and up to 50 characters

Anything outside of these constraints will trigger an error.

Validating the request body with celebrate

Now, we can use the celebrate library to enable joi validation as middleware. Import the package and connect it as a middleware to the route:

const { celebrate, Joi, Segments } = require('celebrate');

app.post(
  "/signup",
  celebrate({
    [Segments.BODY]: Joi.object().keys({
      name: Joi.string().alphanum().min(2).max(30).required(),
      email: Joi.string().required().email(),
      password: Joi.string()
        .pattern(new RegExp("^[a-zA-Z0-9]{3,30}$"))
        .required()
        .min(8),
      repeat_password: Joi.ref("password"),
      age: Joi.number().integer().required().min(18),
      about: Joi.string().min(2).max(30),
    }),
  }),
  (req, res) => {
    // ...
    console.log(req.body);
    res.status(201).send(req.body);
  }
);
Enter fullscreen mode Exit fullscreen mode

Here, we're using celebrate to validate the request body.
Celebrate takes an object in which the key can be one of the values from Segments and the value is a joi schema. Segments is a set of named constants, enum, that can be used to identify the different parts of a request:

{
  BODY: 'body',
  QUERY: 'query',
  HEADERS: 'headers',
  PARAMS: 'params',
  COOKIES: 'cookies',
  SIGNEDCOOKIES: 'signedCookies',
}
Enter fullscreen mode Exit fullscreen mode

Error handling

If we try out our endpoint for signup with a body that doesn't match the schema, we'll get the following error:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Error</title>
</head>
<body>
  <pre>Error: Validation failed<br> &nbsp; &nbsp;at /Users/hulyakarakaya/Desktop/celebrate/node_modules/celebrate/lib/celebrate.js:95:19<br> &nbsp; &nbsp;at processTicksAndRejections (node:internal/process/task_queues:96:5)</pre>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

Celebrate has a special errors() middleware for sending errors to the client. By implementing this middleare, we can send more detailed error messages. Import errors from celebrate and pass it to the app.use method:

const { errors } = require('celebrate');

// celebrate error handler
app.use(errors()); 
Enter fullscreen mode Exit fullscreen mode

This middleware will only handle errors generated by celebrate. Let's see it in action!

Testing the endpoint

We'll use Postman for testing our endpoint. Make sure your server is running before you test the endpoint.

Make a POST request to the /signup route. If we don't correctly repeat the password, we should get an error.

The error status returned by celebrate is 400, and the response body is:

{
    "statusCode": 400,
    "error": "Bad Request",
    "message": "Validation failed",
    "validation": {
        "body": {
            "source": "body",
            "keys": [
                "repeat_password"
            ],
            "message": "\"repeat_password\" must be [ref:password]"
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Or, if we input an age that is lower than 18, we'll get a “Bad Request” error:

{
    "statusCode": 400,
    "error": "Bad Request",
    "message": "Validation failed",
    "validation": {
        "body": {
            "source": "body",
            "keys": [
                "age"
            ],
            "message": "\"age\" must be greater than or equal to 18"
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

The message field allows the client to understand what is wrong with their request. In these cases, celebrate reports that the repeat password is not equal to the original password, and age must be greater than or equal to 18 in the request body.

Validating request query strings

This will work similar to validating request body, but this time we will use Segments.QUERY as a key.

Imagine we want to send user token in the query string when signing up:

app.post(
  "/signup",
  celebrate({
    [Segments.BODY]: Joi.object().keys({
      // validation rules for the body
    }),
    [Segments.QUERY]: {
      token: Joi.string().token().required(),
    },
  }),
  (req, res) => {
    console.log(req.query.token);
    res.status(200).send(req.query.token);
  }
);
Enter fullscreen mode Exit fullscreen mode

When we test the API endpoint, we need to add a token query string to the URL, and it shouldn't be empty. Validating Query String with celebrate

If we don't pass the token query string, celebrate will show an error message:

{
    "statusCode": 400,
    "error": "Bad Request",
    "message": "Validation failed",
    "validation": {
        "query": {
            "source": "query",
            "keys": [
                "token"
            ],
            "message": "\"token\" is required"
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Validating request headers and parameters

In addition to the request body, celebrate allows you to validate headers and parameters:

const { celebrate, Joi } = require('celebrate');

app.delete(
  "/notes/:noteId",
  celebrate({
    // validate parameters
    [Segments.PARAMS]: Joi.object().keys({
      noteId: Joi.string().alphanum().length(12),
    }),
    [Segments.HEADERS]: Joi.object()
      .keys({
        // validate headers
      })
      .unknown(true),
  }),
  (req, res) => {
    // ...
    res.status(204).send();
  }
);
Enter fullscreen mode Exit fullscreen mode

In our example, we're creating a DELETE request to /notes/:noteId. noteId is a parameter, and it should be a 12-character alphanumeric string.

To validate the headers, we can use the Segments.HEADERS key. However, it's hard to know all the headers that can be sent by the client. So, after calling the keys() method, we can use the unknown(true) option to allow unknown headers.

If we try to DELETE a note ID that is less than 12 characters long (http://localhost:3000/notes/123456), we'll get the following error:

{
    "statusCode": 400,
    "error": "Bad Request",
    "message": "Validation failed",
    "validation": {
        "params": {
            "source": "params",
            "keys": [
                "noteId"
            ],
            "message": "\"noteId\" length must be 12 characters long"
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Validating cookies and signed cookies

Celebrate also allows you to validate cookies and signed cookies. To read the cookies on the server, we'll use cookie-parser, the package we installed earlier. Let's connect it as a middleware in the index.js file:

const cookieParser = require("cookie-parser");

const app = express();

app.use(cookieParser("secret"));
Enter fullscreen mode Exit fullscreen mode

Cookies can be stored in local data files. We can set cookies using the res.cookie() method:

res.cookie("name", "john", { httpOnly: true, maxAge: 3600000});
Enter fullscreen mode Exit fullscreen mode

The first argument is the key, and the second, the value. The third argument is an object that contains the options for the cookie. httpOnly: true means that the cookie can't be read from JavaScript and maxAge is the time in milliseconds that the cookie will be valid. So, the cookie will expire after one hour.

Cookie-parser will help us extract the data from the Cookie header and parse the result into an object. We can now access the cookies on the server using the req.cookies object.

Now, we can add our validation to the Segments.COOKIES key:

app.get(
  "/notes",
  celebrate({
    // validate parameters
    [Segments.COOKIES]: Joi.object().keys({
      name: Joi.string().alphanum().min(2).max(30),
    }),
  }),
  function (req, res) {
    res.cookie("name", "john", { httpOnly: true, maxAge: 3600000 });
    console.log("Cookies: ", req.cookies);
    res.send(req.cookies.name);
  }
);
Enter fullscreen mode Exit fullscreen mode

Signed cookies are similar to cookies, but they contain a signature so that the server can verify whether or not the cookie is modified:

app.get(
  "/notes",
  celebrate({
    [Segments.SIGNEDCOOKIES]: Joi.object().keys({
      jwt: Joi.string().alphanum().length(20),
    }),
  }),
  function (req, res) {
    // signed cookie
    res.cookie("jwt", "snfsdfliuhewerewr4i4", { signed: true });
    console.log("Signed Cookies: ", req.signedCookies);
    res.send(req.signedCookies);
  }
);
Enter fullscreen mode Exit fullscreen mode

Here, we've set jwt to be a signed cookie by passing the signed: true option and created a validation rule with Segments.SIGNEDCOOKIES. Now, we can access the signed cookie on the server using the req.signedCookies object. If we try to send a jwt cookie that is less than 20 characters long, we'll get the following error:

{
    "statusCode": 400,
    "error": "Bad Request",
    "message": "Validation failed",
    "validation": {
        "signedCookies": {
            "source": "signedCookies",
            "keys": [
                "jwt"
            ],
            "message": "\"jwt\" length must be 20 characters long"
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

In this post, we've learned why you need to validate user inputs, and how to use joi and celebrate to validate user inputs, headers, query strings, parameters, cookies, and signed cookies. Also, we learned celebrate’s error handling abilities and how to test our endpoints using Postman. I hope you find this tutorial helpful, feel free to let us know in the comments if there is anything that is unclear.


200’s only ✔️ Monitor failed and slow network requests in production

Deploying a Node-based web app or website is the easy part. Making sure your Node instance continues to serve resources to your app is where things get tougher. If you’re interested in ensuring requests to the backend or third party services are successful, try LogRocket.

LogRocket Sign Up

LogRocket is like a DVR for web and mobile apps, recording literally everything that happens while a user interacts with your app. Instead of guessing why problems happen, you can aggregate and report on problematic network requests to quickly understand the root cause.

Top comments (0)