Hello everyone!
In this article I will show you how I use Joi and how I split validation logic when I develop an express app.
What is Joi?
Joi is schema validation library that allows to validate nested JSON object. Check the playground
Some useful articles
Creating Project Folder Structure
mkdir express-joi-validation && cd express-joi-validation
npm init -y
# install packages
npm install --save express http-errors joi
# create folders
mkdir middlewares routes validators
touch app.js
touch routes/auth.js routes/post.js
touch middlewares/Validator.js
touch validators/index.js validators/login.validator.js validators/post.validator.js validators/register.validator.js
Roadmap
- Create
login
,register
,post
joi schemas - Export all schemas as single module(
validators/index.js
) - Create configurable middleware that takes schema name as parameter and validates request body(
middlewares/Validator.js
) - Create
auth
andpost
routes - Create expressjs instance
Implementing Validation Schemas
Let's start with login schema.
Before login process, We'll validate user email and user password. Request body will be like this;
{
"email": "mail@mail.com",
"password": "1234"
}
-
email
field has to be string and valid email -
password
field has to be string and minimum length 4
Also this two fields are required.
//* validators/login.validator.js
const Joi = require('joi')
const loginSchema = Joi.object({
email: Joi.string().email().lowercase().required(),
password: Joi.string().min(5).required()
});
module.exports = loginSchema;
Let's continue with register schema.
For registering a user, We need email
, username
, password
, name
and surname
info. All this fields are required. And here is the schema
//* validators/register.validator.js
const Joi = require('joi');
const registerSchema = Joi.object({
email: Joi.string().email().lowercase().required(),
username: Joi.string().min(1).required(),
password: Joi.string().min(4).required(),
name: Joi.string().min(1).required(),
surname: Joi.string().min(1).required()
});
module.exports = registerSchema;
A user can share a post. A post has title
, content
and tags
fields. Here is an example request body:
{
"title": "A Post Title",
"content": "Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
"tags": ["tag#1", "tag#2"]
}
//* validators/post.validator.js
const Joi = require('joi');
const postSchema = Joi.object({
title: Joi.string().min(5).required(),
content: Joi.string().min(1).required(),
tags: Joi.array().items(Joi.string()).min(2).max(4).required()
});
module.exports = postSchema;
- As you see
tags
field is array of strings. -
tags
min length is 2 and max length is 4
Export All Schemas as Single Module
Require all validators and export them as an object
//* validators/index.js
const register = require('./register.validator')
const login = require('./login.validator')
const post = require('./post.validator')
module.exports = {
register,
login,
post
}
Validator
Middleware
Now let's create a middleware called Validator
and it acts like factory method.
- It takes validator name as parameter.
- If the given validator is not exist, it throws an error.
- If validation error occurs, error handler returns
HTTP 422 Unprocessable Entity
//* middlewares/Validator.js
const createHttpError = require('http-errors')
//* Include joi to check error type
const Joi = require('joi')
//* Include all validators
const Validators = require('../validators')
module.exports = function(validator) {
//! If validator is not exist, throw err
if(!Validators.hasOwnProperty(validator))
throw new Error(`'${validator}' validator is not exist`)
return async function(req, res, next) {
try {
const validated = await Validators[validator].validateAsync(req.body)
req.body = validated
next()
} catch (err) {
//* Pass err to next
//! If validation error occurs call next with HTTP 422. Otherwise HTTP 500
if(err.isJoi)
return next(createHttpError(422, {message: err.message}))
next(createHttpError(500))
}
}
}
How to Use?
Here is the fake auth
endpoints to use
-
[POST] /auth/login
: callValidator('login')
before the response callback -
[POST] /auth/register
: callValidator('register')
before the response callback
//* routes/auth.js
const express = require('express')
const router = express.Router()
const Validator = require('../middlewares/Validator')
router.post('/login', Validator('login'), (req, res, next) => {
//* LET'S MAKE IT MORE REALISTIC
const accessToken = Date.now()
const refreshToken = Date.now()
res.json({ accessToken, refreshToken })
})
router.post('/register', Validator('register'), (req, res, next) => {
//* LET'S MAKE IT MORE REALISTIC
const accessToken = Date.now()
const refreshToken = Date.now()
res.json({ accessToken, refreshToken })
})
module.exports = router
And here is the fake post
endpoint to use Validator
middleware
//* routes/post.js
const express = require('express')
const router = express.Router()
const Validator = require('../middlewares/Validator')
router.post('/', Validator('post'), (req, res, next) => {
res.json({ post: req.body })
})
module.exports = router
Finally, we create the HTTP server.
//* app.js
const http = require('http')
const express = require('express')
const createHttpError = require('http-errors')
const app = express()
const httpServer = http.createServer(app)
//* Routes
const authRouter = require('./routes/auth')
const postRouter = require('./routes/post')
//* Application Level Middlewares
//* Parse JSON body
app.use(express.json())
//* Bind Routes
app.use('/auth', authRouter)
app.use('/posts', postRouter)
//* Catch HTTP 404
app.use((req, res, next) => {
next(createHttpError(404));
})
//* Error Handler
app.use((err, req, res, next) => {
res.status(err.status || 500);
res.json({
error: {
status: err.status || 500,
message: err.message
}
})
});
const PORT = process.env.PORT || 3000
httpServer.listen(3000, () => console.log(`app listening at http://localhost:${PORT}`))
Let's Test
Case 1: Pass non exist schema as parameter
Pass Validator('MyLoginValidator')
to /auth/login
route
Expected output:
Case 2: Testing /posts
Example request body:
{
"title": "title of post",
"content": "content of post",
"tags": ["nodejs", "expressjs", "joi", "validation"]
}
Example request body:
{
"title": "title of post",
"content": "content of post",
"tags": ["nodejs", "expressjs", "joi", "validation", "fail"]
}
Case 3: Testing /auth/register
Example request body:
{
"email": "tayfunakgc"
}
Thanks a lot for reading.
Top comments (4)
good info to share. Thank You.
Hey, i think instead of passing hardcoded strings into the validation function, you should instead import the object with the validators into the respective file with the route in it, and pass in the Joi object itself as a parameter to the validator function. Then call validate on the passed object inside the validator. This also makes it a lot easier to use this middleware in a typescript function, and removes the first if statement in the check valid function.
Thanks, That is exactly what i went for.
Hi,
Can you do above same project in TypeScript instead of JavaScript. Beacuse i did my project in Node.js + TypeScript so when i convert it TS then its fail.
Thanks