Validate request data with @novice1/validator-joi

shygyver

ShyGyver

Posted on October 29, 2023

Validate request data with @novice1/validator-joi

In the previous post, we learnt how to setup a Rest API with @novice1/app and create routes and services. Now we will see how to respond different status code and validate request data.

Middlewares

First we want to handle http errors with middlewares.
We are going to start by updating our debug service to export a logger for middlewares.

src/services/debug.ts

import logger from '@novice1/logger';

logger.Debug.enable([
    'route*',
    'middleware*'
].join('|'));

export const debugRoute = logger.debugger('route');
export const debugMiddleware = logger.debugger('middleware');
Enter fullscreen mode Exit fullscreen mode

Now let's handle http errors 404 and 500.
First, we create the functions.

src/middlewares/http.ts

import { ErrorRequestHandler, RequestHandler } from 'express'
import { debugMiddleware } from '../services/debug'

export const httpError: ErrorRequestHandler = (err, _req, res, _) => {
    const logger = debugMiddleware.extend('httpError');
    logger.error(err);
    res.status(500).json({message: 'Something went wrong'})
}

export const httpNotFound: RequestHandler = (_req, res) => {
    res.status(404).json({message: 'Not found'});
}
Enter fullscreen mode Exit fullscreen mode

Then we register them to the app server with the methods use and useError (special method to register ErrorRequestHandler).

src/index.ts

import logger from '@novice1/logger';
import { app } from './app';
import { httpError, httpNotFound } from './middlewares/http';

const PORT = process.env.PORT ? parseInt(process.env.PORT) : 8000;

// 404
app.use(httpNotFound);

// error
app.useError(httpError);

// start server
app.listen(PORT, () => {
    logger.info('Application running on port', PORT)
})
Enter fullscreen mode Exit fullscreen mode

We can check that it works by testing the Not Found handler (e.g.: http://localhost:8080/path-that-does-not-exist).

Validate requests

We can validate requests many ways, @novice1/app being as flexible as Express. We will use @novice1/validator-joi, a validator made expressly for @novice1/routing.

We install the dependencies

npm install joi @novice1/validator-joi
Enter fullscreen mode Exit fullscreen mode

and register the validator globaly in the app (it could also be registered by router, see more examples here)

src/app.ts

import { FrameworkApp } from '@novice1/app';
import cookieParser from 'cookie-parser';
import express from 'express';
import cors from 'cors';
import routes from './routes';
import validatorJoi from '@novice1/validator-joi'
import { debugMiddleware } from './services/debug';

// init app
export const app = new FrameworkApp({
    framework: {
        // middlewares for all requests
        middlewares: [
            cookieParser(),
            express.json(),
            express.urlencoded({ extended: true }),
            cors()
        ],
        validators: [validatorJoi(undefined, (err, _req, res) => {
            debugMiddleware.extend('validator-joi').error(err)
            res.status(400).json({message: "bad request"})
        })]
    },
    routers: routes
})
Enter fullscreen mode Exit fullscreen mode

There we:

  • registered our validator in the property framework.validators
  • used a custom error request handler to respond with a status code 400 if data aren't valid (see @novice1/validator-joi)

Now all we need are routes to validate.
Below is an example of http CRUD operations:

src/routes/items.ts

import routing from '@novice1/routing'
import { debugRoute, debugMiddleware } from '../services/debug'
import Joi from 'joi';
import { ErrorRequestHandler } from 'express';

const logger = debugRoute.extend('items')

const customErrorHandler: ErrorRequestHandler = (err, _req, res, _) => {
    const log = debugMiddleware.extend('customErrorHandler');
    log.error(err);
    res.status(400).json({message: 'Bad request: could not update an item'})
}

const itemsRouter = routing()
    .get({
        path: '/'
    }, (_, res) => {
        res.json([])
    })
    .post({
        path: '/',
        parameters: {
            body: Joi.object().keys({
                title: Joi.string().required().min(1).max(128),
                description: Joi.string().max(2560).allow(''),
                text: Joi.string().max(5120).allow(''),
                published: Joi.boolean().default(false)
            })
        }
    }, (req, res) => {
        const item = req.body;
        res.json(item);
    });

const itemsIdRouter = routing()
    .get({
        path: '/:id',
        parameters: {
            params: {
                id: Joi.string().required()
            },
        }
    }, (req, res) => {
        res.json({ message: `Got "${req.params.id}"` });
    })
    .put({ 
        path: '/:id',
        parameters: {
            params: {
                id: Joi.string().required()
            },
            body: Joi.object().keys({
                title: Joi.string().min(1).max(128),
                description: Joi.string().max(2560).allow(''),
                text: Joi.string().max(5120).allow(''),
                published: Joi.boolean()
            }).min(1),
            onerror: customErrorHandler
        }
    }, (req, res) => {
        const item = req.body;
        item.id = req.params.id;
        res.json(item);
    })
    .delete({
        path: '/:id',
        parameters: {
            params: {
                id: Joi.string().required()
            },
        }
    }, (req, res) => {
        res.json({ id: req.params.id });
    })

export default routing().use('/items', (req, _res, next) => {
    logger.debug(`${req.method} ${req.originalUrl}`)
    next()
}, itemsRouter, itemsIdRouter);
Enter fullscreen mode Exit fullscreen mode

There we:

  • defined routers for the path /items
  • configured path and parameters for each route

We use the joi package to write the properties' schema that will be validated by our validator (so some knowledge of joi is required).

@novice1/validator-joi can validate the properties params, body, query, headers, cookies, and files (useful when using multer) from the request.

For one route, we defined an Error Request Handler (customErrorHandler) in the parameter onerror. It is useful if we want to do something specific when a validation error happens on that route.

Note: As you can see, we didn't make any operation to a database as it is not the purpose of the example but you can complete the controllers as you wish.

Finally, let's register the new router

src/routes/index.ts

import corsOptions from './cors-options';
import helloWorld from './hello-world';
import items from './items';

// all routers
export default [
    corsOptions,
    helloWorld,
    items
]
Enter fullscreen mode Exit fullscreen mode

Now we can run the application and try our different routes (with a tool like Postman for example) and send invalid data to see if our validator is working and responds with a status code 400.

For development:

npm run dev
Enter fullscreen mode Exit fullscreen mode

For production:

npm run lint
npm run build
npm start
Enter fullscreen mode Exit fullscreen mode

References

You can see the result of what we have done till now right here on Github.

💖 💪 🙅 🚩
shygyver
ShyGyver

Posted on October 29, 2023

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

Sign up to receive the latest update from our blog.

Related

Validator libraries in Nodejs
javascript Validator libraries in Nodejs

November 20, 2024

Awesome NestJS boilerplate
javascript Awesome NestJS boilerplate

July 5, 2019