TypeScript Express tutorial #3. Error handling and validating incoming data

108 阅读2分钟

Express Error handling middleware

Defining HttpException

// src/exceptions/HttpException.ts

class HttpException extends Error {
    status: number;
    message: string;
    constructor(status: number, message: string) {
      super(message);
      this.status = status;
      this.message = message;
    }
  }
   
  export default HttpException;

Defining Express error handling middleware

// src/middleware/error.middleware.ts

import { NextFunction, Request, Response } from 'express';
import HttpException from '../exceptions/HttpException';
 
function errorMiddleware(error: HttpException, request: Request, response: Response, next: NextFunction) {
  const status = error.status || 500;
  const message = error.message || 'Something went wrong';
  response
    .status(status)
    .send({
      status,
      message,
    })
}
 
export default errorMiddleware;

Since Express runs all the middleware from the first to the last, your error handlers should be at the end of your application stack. If you pass the error to the next function, the framework omits all the other middleware in the chain and skips straight to the error handling middleware which is recognized by the fact that it has four arguments.

use the error middleware

// src/app.ts

import * as bodyParser from 'body-parser';
import  express from 'express';
import * as mongoose from 'mongoose';
import Controller from './interfaces/controller.interface';
import errorMiddleware from './middleware/error.middleware';
 
class App {
  public app: express.Application;
 
  constructor(controllers: Controller[]) {
    this.app = express();
 
    this.connectToTheDatabase();
    this.initializeMiddlewares();
    this.initializeControllers(controllers);
    this.initializeErrorHandling();
  }
 
  public listen() {
    this.app.listen(process.env.PORT, () => {
      console.log(`App listening on the port ${process.env.PORT}`);
    });
  }
 
  private initializeMiddlewares() {
    this.app.use(bodyParser.json());
  }

  private initializeErrorHandling() {
    this.app.use(errorMiddleware);
  }
 
  private initializeControllers(controllers: Controller[]) {
    controllers.forEach((controller) => {
      this.app.use('/', controller.router);
    });
  }
 
  private connectToTheDatabase() {
    const {
        MONGO_USER,
        MONGO_PASSWORD,
        MONGO_PATH,
        MONGO_DB_NAME,
      } = process.env;


const url = `mongodb://${MONGO_USER}:${MONGO_PASSWORD}@${MONGO_PATH}/${MONGO_DB_NAME}?authSource=admin`;

console.log(url);

mongoose.connect(url);
  }
}
 
export default App;
// src/service/posts/post.service.ts
...
import HttpException from '../../exceptions/HttpException';

...
export async function getPostById(
	request: express.Request,
	response: express.Response,
	next: express.NextFunction
) {
	const id = request.params.id;
	const post = await postModel.findById(id).exec();

	if (post) {
		response.send(post);
	} else {
		next(new HttpException(404, "Post not found"));
	}
}

export async function modifyPost(
	request: express.Request,
	response: express.Response,
	next: express.NextFunction
) {
	const id = request.params.id;
	const postData: Post = request.body;

	const post = await postModel
		.findByIdAndUpdate(id, postData, { new: true })
		.exec();

	if (post) {
		response.send(post);
	} else {
		next(new HttpException(404, "Post not found"));
	}
}

export async function deletePost(
	request: express.Request,
	response: express.Response,
    next: express.NextFunction
) {
	const id = request.params.id;

	const post = await postModel.findByIdAndDelete(id).exec();

	if (post) {
		response.send(200);
	} else {
		next(new HttpException(404, "Post not found"));
	}
}
...


defining PageNotFonund

// src/exceptions/PostNotFoundException.ts


import HttpException from "./HttpException";
 
class PostNotFoundException extends HttpException {
  constructor(id: string) {
    super(404, `Post with id ${id} not found`);
  }
}
 
export default PostNotFoundException;

refact

export async function getPostById(
	request: express.Request,
	response: express.Response,
	next: express.NextFunction
) {
	const id = request.params.id;
	const post = await postModel.findById(id).exec();

	if (post) {
		response.send(post);
	} else {
        next(new PostNotFoundException(id));
	}
}

export async function modifyPost(
	request: express.Request,
	response: express.Response,
	next: express.NextFunction
) {
	const id = request.params.id;
	const postData: Post = request.body;

	const post = await postModel
		.findByIdAndUpdate(id, postData, { new: true })
		.exec();

	if (post) {
		response.send(post);
	} else {
		next(new PostNotFoundException(id));
	}
}

export async function deletePost(
	request: express.Request,
	response: express.Response,
    next: express.NextFunction
) {
	const id = request.params.id;

	const post = await postModel.findByIdAndDelete(id).exec();

	if (post) {
		response.send(200);
	} else {
		next(new PostNotFoundException(id));
	}
}

update controller

// src/controller/posts/post.controller.ts

...
 private getPostById = (request: express.Request, response: express.Response, next: express.NextFunction) => {
    this.postService.getPostById(request, response, next);
  }
 
  private modifyPost = (request: express.Request, response: express.Response, next: express.NextFunction) => {
    this.postService.modifyPost(request, response, next)
  }
...  
    private deletePost = (request: express.Request, response: express.Response, next: express.NextFunction) => {
    this.postService.deletePost(request, response, next);
  }
...

Validating incoming data

for more usage ,read the doc of class-validator

To use decorators with TypeScript, you need to add  "experimentalDecorators": true to your tsconfig.json

// tsconfig.json
{
  "compilerOptions": {
  ...
  "experimentalDecorators": true,
  ...
  
  }
  
}
pnpm i class-validator class-transformer reflect-metadata es6-shim

update entry

// src/server.ts

import 'dotenv/config';
import App from './app';
import validateEnv from './utils/validateEnv';
import PostsController from './controller/posts/post.controller';

// class-transformer dependency
import 'reflect-metadata';
import 'es6-shim';
// class-transformer

 
validateEnv();
 
const app = new App(
  [
    new PostsController(),
  ],
);
 
app.listen();

create dto

// src/dto/posts/post.dto.ts


import { IsString } from 'class-validator';
 
class CreatePostDto {
  @IsString()
  public author?: string;
 
  @IsString()
  public content?: string;
 
  @IsString()
  public title?: string;
}
 
export default CreatePostDto;

create validation middleware

// src/middleware/validation.middleware.ts

import { plainToClass } from 'class-transformer';
import { validate, ValidationError } from 'class-validator';
import express from 'express';
import HttpException from '../exceptions/HttpException';
 
function validationMiddleware<T>(type: any): express.RequestHandler {
  return (req, res, next) => {
    validate(plainToClass(type, req.body))
      .then((errors: ValidationError[]) => {
        if (errors.length > 0) {
          const message = errors.map((error: ValidationError) => error.constraints && Object.values(error.constraints)).join(', ');
          next(new HttpException(400, message));
        } else {
          next();
        }
      });
  };
}
 
export default validationMiddleware;

attach the middleware

// src/controller/posts/post.controller.ts

import validationMiddleware from '../../middleware/validation.middleware';
import CreatePostDto from '../../dto/posts/post.dto';

class PostsController implements Controller {

  ...
  
  private initializeRoutes() {
    this.router.get(this.path, this.getAllPosts);
    this.router.get(`${this.path}/:id`, this.getPostById);
    this.router.patch(`${this.path}/:id`, this.modifyPost);
    this.router.delete(`${this.path}/:id`, this.deletePost);
    this.router.post(this.path, validationMiddleware(CreatePostDto),this.createPost);
  }
  
  ...
}

here Error handling and validating incoming data procedure is shown,

class-transformer may not be flexiable as you need.

ref: sources