Let’s say we have an authentication service simply like below. This service is listening on port 3000 and having a route for signing up user account.
import express from 'express'
import { json } from 'body-parser'
import { signupRouter } from './routes/signup';
const app = express();
app.use(json())
app.use(signupRouter);
app.listen(3000, () => {
console.log('listening on port 3000. hello')
})
Below is signup route.
import express, { Request, Response } from 'express'
const router = express.Router()
router.post('/api/users/signup', (req, res) => {
console.log('Creating a user...')
res.send({})
})
export { router as signupRouter };
Now we want to add some validation for the parameters. Normally we will have a client in React or Angular which sends the parameters including email and password. We might implement the validation in the callback function like this.
router.post('/api/users/signup', (req, res) => {
const { email, password } = req.body
if (!email || typeof email !== 'string') {}
})
Doing like above is not efficient. Express provides us a better way by using express-validator. Let’s install this library and follow the document to implement the validation.
import { body, validationResult } from 'express-validator'
router.post('/api/users/signup', [
body('email')
.isEmail()
.withMessage('Email is invalid'),
body('password')
.trim()
.isLength({min: 4, max: 20})
.withMessage('Password must be between 4 and 40 chars')
], (req: Request, res: Response) => {
})
export { router as signupRouter };
It acts like a middleware to validate the parameters and transfer the error to callback function. When can implement to return the errors to client.
(req: Request, res: Response) => {
const errors = validationResult(req);
if (!errors.isEmpty()){
throw new Error(errors.array())
}
}
However, it is not simple like this. As a backend server which serves the frontend service, it must have a standard structure of error so the client can understand. Let’s say we need to return the error as the structure like this all the time.
{ message: string, field?: string}[]
{
"errors": [
{
"message": "Email is invalid",
"field": "email"
}
]
}
We also need to apply the single responsibility principle to error handling. So we create a class for each error type and make sure they return the same structure of response. In order to do this, we use abstract class for custom error. The abstract class here for your reference.
export abstract class CustomError extends Error {
abstract statusCode: number;
constructor(message: string) {
super(message)
Object.setPrototypeOf(this, CustomError.prototype)
}
abstract serializeErrors(): { message: string, field?: string}[]
}
Now, any class extends this class must declare the statusCode and serializeErrors method with the same structure of response.
In callback function, we threw a error which is the implementation class extending CustomError.
(req: Request, res: Response) => {
const errors = validationResult(req);
if (!errors.isEmpty()){
throw new RequestValidationError(errors.array())
}
res.send({})
}
Eventually, we use an error handler which checks the err variable. The RequestValidationError which extends the abstract class CustomerError so the statement (err instanceof CustomError) returns true. We are sure of having statusCode and serializeErrors method. Thus, we implement the custom error handler function of express.
import { NextFunction, Request, Response } from "express";
import { CustomError } from "../errors/custom-error";
export const errorHandler = (
err: Error,
req: Request,
res: Response,
next: NextFunction) => {
// console.log(err)
if (err instanceof CustomError) {
return res.status(err.statusCode).send({errors: err.serializeErrors()})
}
if (err instanceof CustomError) {
return res.status(err.statusCode).send({errors: err.serializeErrors()});
}
res.status(400).send({
error: 'Something went wrong'
})
}
Here is the implementation of custom error for your reference.
import { ValidationError } from "express-validator";
import { CustomError } from './custom-error';
export class RequestValidationError extends CustomError {
statusCode = 400
constructor(public errors: ValidationError[]) {
super('Invalid request parameters');
Object.setPrototypeOf(this, RequestValidationError.prototype)
}
serializeErrors() {
return this.errors.map(err => {
return { message: err.msg, field: err.param}
})
}
}