DEV Community

Tayfun Akgüç
Tayfun Akgüç

Posted on • Updated on

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.

Top comments (4)

Collapse
 
souhaildev profile image
Souhail Dev

good info to share. Thank You.

Collapse
 
leifetter profile image
LeifEtter

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.

const checkValid = (validationObject: Joi.ObjectSchema) =>
  async function (req: Request, res: Response, next: NextFunction) {
    try {
      await validationObject.validateAsync(req.body);
      next();
    } catch (error) {
      if (error instanceof Joi.ValidationError) {
        return res.status(400).send({ message: error.message });
      }
      console.error(error);
      return res
        .status(500)
        .send({ message: "Something went wrong during validation" });
    }
  };
Enter fullscreen mode Exit fullscreen mode
import validation from "../helpers/validation";
userRouter.route("/login").get(checkValid(validation.login), login);
Enter fullscreen mode Exit fullscreen mode
Collapse
 
prospercoded profile image
Prosper Coded • Edited

Thanks, That is exactly what i went for.

Collapse
 
sherinmac profile image
SHERIN AG

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