Hi everyone,
In today's post, we will explore the concept of code validation in Node.js and Express, focusing on the powerful tool called Joi. Writing validation conditions manually can be error-prone and lead to complex, hard-to-read code. Fortunately, Joi offers a solution by making data validation easier to implement and manage.
One of the key benefits of Joi is its usability. With its simple syntax and seamless integration with JavaScript, using Joi to create blueprints for JavaScript objects ensures accurate and reliable data processing. By leveraging Joi, we can simplify the validation process and streamline our code.
In this post, we will:
Explore the features of Joi, highlighting its capabilities and advantages.
Demonstrate how we can effectively incorporate Joi into the backend Request Pipeline, ensuring robust data validation.
Let's dive into the world of Joi and discover how it can enhance our code validation practices in Node.js and Express!
Introducing Joi
Installing Joi is quite easy. We just need to type:
npm install joi
After that, we are ready to use it. Let’s have a quick look at how we use it. The first thing we do is import it and then we set up some rules, like so
const Joi = require('joi');
const schema = Joi.object().keys({
name: Joi.string().alphanum().min(3).max(30).required(),
birthyear: Joi.number().integer().min(1970).max(2013),
});
const dataToValidate = {
name 'chris',
birthyear: 1971
}
const result = Joi.validate(dataToValidate, schema);
// result.error == null means valid
What we are looking at above is us doing the following:
constructing a schema, our call to Joi.object(),
validating our data, our call to Joi.validate()
with dataToValidate
and schema as input parameters
Ok, now we understand the basic motions. What else can we do?
Well Joi supports all sorts of primitives as well as Regex and can be nested to any depth. Let’s list some different constructs it supports:
string, this says it needs to be of type string, and we use it like so Joi.string()
number, Joi.number() and also supporting helper operations such as min() and max(), like so Joi.number().min(1).max(10)
required, we can say whether a property is required with the help of the method required, like so Joi.string().required()
any, this means it could be any type, usually we tend to use it with the helper allow() that specifies what it can contain, like so, Joi.any().allow('a')
optional, this is strictly speaking not a type but has an interesting effect. If you specify for example prop : Joi.string().optional. If we don't provide prop then everybody's happy. However if we do provide it and make it an integer the validation will fail
array, we can check wether the property is an array of say strings, then it would look like this Joi.array().items(Joi.string().valid('a', 'b')
regex, it supports pattern matching with RegEx as well like so Joi.string().regex(/^[a-zA-Z0-9]{3,30}$/)
The whole API for Joi is enormous. I suggest to have a look and see if there is a helper function that can solve whatever case you have that I’m not showing above
Joi API: https://github.com/hapijs/joi/blob/v14.3.1/API.md
## Nested types
Ok, so we have only shown how to declare a schema so far that is one level deep. We did so by calling the following:
Joi.object().keys({ });
This stated that our data is an object. Then we added some properties to our object like so:
Joi.object().keys({
name: Joi.string().alphanum().min(3).max(30).required(),
birthyear: Joi.number().integer().min(1970).max(2013)
});
Now, nested structures are really more of the same. Let’s create an entirely new schema, a schema for a blog post, looking like this:
const blogPostSchema = Joi.object().keys({
title: Joi.string().alphanum().min(3).max(30).required(),
description: Joi.string(),
comments: Joi.array().items(Joi.object.keys({
description: Joi.string(),
author: Joi.string().required(),
grade: Joi.number().min(1).max(5)
}))
});
Note especially the comments property, that thing looks exactly like the outer call we first make and it is the same. Nesting is as easy as that.
Libraries like these are great but wouldn’t it be even better if we could use them in a more seamless way, like in a Request pipeline? Let’s have a look firstly how we would use Joi in an Express app in Node.js:
const Joi = require('joi');
app.post('/blog', async (req, res, next) => {
const { body } = req; const
blogSchema = Joi.object().keys({
title: Joi.string().required
description: Joi.string().required(),
authorId: Joi.number().required()
});
const result = Joi.validate(body, blogShema);
const { value, error } = result;
const valid = error == null;
if (!valid) {
res.status(422).json({
message: 'Invalid request',
data: body
})
} else {
const createdPost = await api.createPost(data);
res.json({ message: 'Resource created', data: createdPost })
}
});
The above works. But we have to, for each route:
create a schema
call validate()
It’s, for lack of better word, lacking in elegance. We want something slick looking.
Let’s see if we can’t rebuild it a bit to a middleware. Middlewares in Express is simply something we can stick into the request pipeline whenever we need it. In our case we would want to try and verify our request and early on determine whether it is worth proceeding with it or abort it.
So let’s look at a middleware. It’s just a function right:
const handler = (req, res, next) = { // handle our request }
const middleware = (req, res, next) => { // to be defined }
app.post( '/blog', middleware, handler )
It would be neat if we could provide a schema to our middleware so all we had to do in the middleware function was something like this:
(req, res, next) => {
const result = Joi.validate(schema, data)
}
We could create a module with a factory function and module for all our schemas. Let’s have a look at our factory function module first:
const Joi = require('joi');
const middleware = (schema, property) => {
return (req, res, next) => {
const { error } = Joi.validate(req.body, schema);
const valid = error == null;
if (valid) {
next();
} else {
const { details } = error;
const message = details.map(i => i.message).join(',');
console.log("error", message);
res.status(422).json({ error: message }) }
}
}
module.exports = middleware;
Let’s thereafter create a module for all our schemas, like so:
// schemas.js
const Joi = require('joi')
const schemas = {
blogPOST: Joi.object().keys({
title: Joi.string().required
description: Joi.string().required()
})
// define all the other schemas below
};
module.exports = schemas;
Ok then, let’s head back to our application file:
// app.js
const express = require('express')
const cors = require('cors');
const app = express()
const port = 3000
const schemas = require('./schemas');
const middleware = require('./middleware');
var bodyParser = require("body-parser");
app.use(cors());
app.use(bodyParser.json());
app.get('/', (req, res) => res.send('Hello World!'))
app.post('/blog', middleware(schemas.blogPOST) , (req, res) => {
console.log('/update');
res.json(req.body);
});
app.listen(port, () => console.log(`Example app listening on port ${port}!`))
Docker: Managing data with Volumes (Part 2) Hello everyone, In the last post, we discussed the concept of volumes. We discussed the use of named and anonymous volumes. In this post, we will discuss the need and use of bind mounts We will learn the use of .dockerignore files. We will also see working with environment variables and .env files. We will also learn the use of build arguments.
Docker: Managing data with Volumes (Part 1) Hello everyone, In the last post, we learned to perform various operations with containers and images. In this post, we will explore volumes, which are like state management for running applications with docker. We will discuss different kinds of data we encounter while working with docker. We will learn about different kinds of volumes, and explore anonymous and named volumes in detail by discussing their implementation in a node server application. We will create these volumes in our system.