Middleware Based Joi Validation in ExpressJS
Published: 2021-08-15 23:03:32

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](https://joi.dev/tester/)

Some useful articles 
* [Validation; joi brings you "Joy"](https://dev.to/olufemi/validation-joi-brings-you-joy-fof)
* [Joi — awesome code validation for Node.js and Express](https://dev.to/itnext/joi-awesome-code-validation-for-node-js-and-express-35pk)



## Creating Project Folder Structure
```bash
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
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;

```json
{
    "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. 

```js
//* 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

```js
//* 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:

```js
{
    "title": "A Post Title",
    "content": "Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
    "tags": ["tag#1", "tag#2"]
}
```

```js
//* 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

```js
//* 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` 

```js
//* 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

1. `[POST] /auth/login`: call `Validator('login')` before the response callback
2. `[POST] /auth/register`: call `Validator('register')` before the response callback



```js
//* 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

```js
//* 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.

```js
//* 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: 

![Non Exist Schema](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/f4m3unj60i3yl651f8ha.png)

### Case 2: Testing `/posts`

Example request body: 
```json
{
    "title": "title of post",
    "content": "content of post",
    "tags": ["nodejs", "expressjs", "joi", "validation"]
}
```
Expected output:
![Post Created](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wb49xne32prdy6pdcvrz.png)

Example request body:
```json
{
    "title": "title of post",
    "content": "content of post",
    "tags": ["nodejs", "expressjs", "joi", "validation", "fail"]
}
```
![Fail /posts](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/m3trhh96q21p9ibr1iyb.png)

### Case 3: Testing `/auth/register`
Example request body:
```json
{
    "email": "tayfunakgc"
}
```
![Fail /auth/register](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xu7fi82qo1c6d07z7pkh.png)


[Github repository](https://github.com/tayfunakgc/express-joi-validation)

Thanks a lot for reading.