Validate request data with @novice1/validator-joi
Tags: javascript, beginners, node, typescript
Published: 2023-10-29 17:03:33

In the [previous post](https://dev.to/shygyver/nodejs-rest-api-with-novice1app-3a0d), we learnt how to setup a Rest API with [@novice1/app](https://www.npmjs.com/package/@novice1/app) and create [routes](https://dev.to/shygyver/nodejs-rest-api-with-novice1app-3a0d#routes) and [services](https://dev.to/shygyver/nodejs-rest-api-with-novice1app-3a0d#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_

```ts
import logger from '@novice1/logger';

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

export const debugRoute = logger.debugger('route');
export const debugMiddleware = logger.debugger('middleware');
```

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

_src/middlewares/http.ts_

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

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

_src/index.ts_

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

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

## Validate requests

We can validate requests many ways, [@novice1/app](https://www.npmjs.com/package/@novice1/app#usage) being as flexible as `Express`. We will use [@novice1/validator-joi](https://www.npmjs.com/package/@novice1/validator-joi), a validator made expressly for `@novice1/routing`.

We install the dependencies

```sh
npm install joi @novice1/validator-joi
```

and register the validator globaly in the app (it could also be registered by router, see more examples [here](https://www.npmjs.com/package/@novice1/validator-joi#installation))

_src/app.ts_

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

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](https://www.npmjs.com/package/@novice1/validator-joi#installation))

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

_src/routes/items.ts_

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

There we:
- defined routers for the path `/items`
- configured `path` and `parameters` for each route

We use the [`joi`](https://www.npmjs.com/package/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](https://www.npmjs.com/package/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_

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

// all routers
export default [
    corsOptions,
    helloWorld,
    items
]
```

Now we can run the application and try our different routes (with a tool like [Postman](https://www.postman.com/) for example) and send invalid data to see if our validator is working and responds with a `status code 400`.

For development:

```sh
npm run dev
```

For production:

```sh
npm run lint
npm run build
npm start
```

## References

You can see the result of what we have done till now [right here on Github](https://github.com/shygyver/nodejs-tuto/tree/part-02).