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:
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.
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.
Begin by opening up your terminal and navigating to the directory where you want to place your project:
mkdir notes && cd notes
Create a new Node project by running:
npm init -y
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
Let’s review our installed packages:
req.body
req.cookies
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
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}`); });
Here, we’ve imported Express and BodyParser and invoked the Express function to create our server. The server will listen on port 3000.
Go to your package.json
file and add a script to run our server with nodemon
:
"scripts": { "start": "nodemon index.js" }
Now, we can run our server from terminal by running npm start
. This will start nodemon and watch for changes in our code.
Now that our application is listening for requests, we can create some routes:
/signup
for creating a new user account/notes
for retrieving the notes/notes/:noteId
for deleting a noteNext, we’ll look at how to validate the request data via joi and celebrate.
We can install joi and celebrate via npm like so:
npm install joi celebrate
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}$')).required().min(8), repeat_password: Joi.ref('password'), age: Joi.number().integer().required().min(18), about: Joi.string().min(2).max(30), }) }
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 formatpassword
, a required string with at least eight characters, which should match the custom regex patternrepeat_password
, which should match the passwordage
, a required number with an integer value of 18 or moreabout
, a string with at least two and up to 50 charactersAnything outside of these constraints will trigger an error.
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); } );
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', }
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> at /Users/hulyakarakaya/Desktop/celebrate/node_modules/celebrate/lib/celebrate.js:95:19<br> at processTicksAndRejections (node:internal/process/task_queues:96:5)</pre> </body> </html>
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());
This middleware will only handle errors generated by celebrate. Let’s see it in action!
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]" } } }
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" } } }
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.
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); } );
When we test the API endpoint, we need to add a token
query string to the URL, and it shouldn’t be empty.
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" } } }
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(); } );
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" } } }
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"));
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});
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); } );
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); } );
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" } } }
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.
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 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.
LogRocket instruments your app to record baseline performance timings such as page load time, time to first byte, slow network requests, and also logs Redux, NgRx, and Vuex actions/state. Start monitoring for free.
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowReact Native’s New Architecture offers significant performance advantages. In this article, you’ll explore synchronous and asynchronous rendering in React Native through practical use cases.
Build scalable admin dashboards with Filament and Laravel using Form Builder, Notifications, and Actions for clean, interactive panels.
Break down the parts of a URL and explore APIs for working with them in JavaScript, parsing them, building query strings, checking their validity, etc.
In this guide, explore lazy loading and error loading as two techniques for fetching data in React apps.
3 Replies to "How to use celebrate with Node.js"
AJV is much better and cleaner to use.
Nice article but Express-validator is way easier and easy on the eyes
Where is the dark theme ?