Validate request data with @novice1/validator-joi
ShyGyver
Posted on October 29, 2023
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');
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'});
}
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)
})
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
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
})
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);
There we:
- defined routers for the path
/items
- configured
path
andparameters
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
]
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
For production:
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.
Posted on October 29, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.