Middleware Based Joi Validation in ExpressJS

tayfunakgc

Tayfun Akgüç

Posted on August 15, 2021

Middleware Based Joi Validation in ExpressJS

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



Enter fullscreen mode Exit fullscreen mode

Roadmap

  1. Create login, register, post joi schemas
  2. Export all schemas as single module(validators/index.js)
  3. Create configurable middleware that takes schema name as parameter and validates request body(middlewares/Validator.js)
  4. Create auth and post routes
  5. 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"
}


Enter fullscreen mode Exit fullscreen mode
  • 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;


Enter fullscreen mode Exit fullscreen mode

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;


Enter fullscreen mode Exit fullscreen mode

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"]
}


Enter fullscreen mode Exit fullscreen mode


//* 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;


Enter fullscreen mode Exit fullscreen mode
  • 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
}


Enter fullscreen mode Exit fullscreen mode

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))
        }
    }
}



Enter fullscreen mode Exit fullscreen mode

How to Use?

Here is the fake auth endpoints to use

  1. [POST] /auth/login: call Validator('login') before the response callback
  2. [POST] /auth/register: call Validator('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


Enter fullscreen mode Exit fullscreen mode

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


Enter fullscreen mode Exit fullscreen mode

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}`))


Enter fullscreen mode Exit fullscreen mode

Let's Test

Case 1: Pass non exist schema as parameter

Pass Validator('MyLoginValidator') to /auth/login route
Expected output:

Non Exist Schema

Case 2: Testing /posts

Example request body:



{
    "title": "title of post",
    "content": "content of post",
    "tags": ["nodejs", "expressjs", "joi", "validation"]
}


Enter fullscreen mode Exit fullscreen mode

Expected output:
Post Created

Example request body:



{
    "title": "title of post",
    "content": "content of post",
    "tags": ["nodejs", "expressjs", "joi", "validation", "fail"]
}


Enter fullscreen mode Exit fullscreen mode

Fail /posts

Case 3: Testing /auth/register

Example request body:



{
    "email": "tayfunakgc"
}


Enter fullscreen mode Exit fullscreen mode

Fail /auth/register

Github repository

Thanks a lot for reading.

💖 💪 🙅 🚩
tayfunakgc
Tayfun Akgüç

Posted on August 15, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

What was your win this week?
weeklyretro What was your win this week?

November 29, 2024

Where GitOps Meets ClickOps
devops Where GitOps Meets ClickOps

November 29, 2024

How to Use KitOps with MLflow
beginners How to Use KitOps with MLflow

November 29, 2024

Modern C++ for LeetCode 🧑‍💻🚀
leetcode Modern C++ for LeetCode 🧑‍💻🚀

November 29, 2024