- 1. TypeScript Express tutorial #1. Middleware, routing, and controllers
- 2. TypeScript Express tutorial #2. MongoDB, models and environment variables
- 3. TypeScript Express tutorial #3. Error handling and validating incoming data
- 4. TypeScript Express tutorial #4. Registering users and authenticating with JWT
- 5. TypeScript Express tutorial #5. MongoDB relationships between documents
- 6. TypeScript Express tutorial #6. Basic data processing with MongoDB aggregation
- 7. TypeScript Express tutorial #7. Relational databases with Postgres and TypeORM
- 8. TypeScript Express tutorial #8. Types of relationships with Postgres and TypeORM
- 9. TypeScript Express tutorial #9. The basics of migrations using TypeORM and Postgres
- 10. TypeScript Express tutorial #10. Testing Express applications
- 11. TypeScript Express tutorial #11. Node.js Two-Factor Authentication
- 12. TypeScript Express tutorial #12. Creating a CI/CD pipeline with Travis and Heroku
- 13. TypeScript Express tutorial #13. Using Mongoose virtuals to populate documents
- 14. TypeScript Express tutorial #14. Code optimization with Mongoose Lean Queries
- 15. TypeScript Express tutorial #15. Using PUT vs PATCH in MongoDB with Mongoose
Since not always everything goes perfectly, you need to expect the unexpected. To prepare for that, we cover TypeScript Express error handling and incoming data validation. Just as before, the repository for the tutorial is mwanago/express-typescript. If you find it helpful, feel free to give it a star.
Express Error handling
In the previous part of the tutorial, we wrote a handler function to return a post with a specific ID:
1 2 3 4 5 6 7 |
private getPostById = (request: express.Request, response: express.Response) => { const id = request.params.id; this.post.findById(id) .then((post) => { response.send(post); }); } |
There is a slight problem with it though. If a post with a particular ID does not exist, its value in the callback function is null. We then send it, resulting in a response with a code 200 OK. It indicates that everything went fine and the user got a document that he requests, but this post is not empty in our database: it just does not exist. There is a big list of status codes that you can use, but in this case, we use 404 Not Found.
The 200 OK code if default when you use the send function. To change that, you need to call the status function, before using send.
1 2 3 4 5 6 7 8 9 10 11 |
private getPostById = (request: express.Request, response: express.Response) => { const id = request.params.id; this.post.findById(id) .then((post) => { if (post) { response.send(post); } else { response.status(404).send({ error: 'Post not found' }); } }); } |
Now when someone tries to access a post that does not exist, he is informed about what went wrong.
This outcome is good, but we can make our code better. We want to create errors from our route handlers and let the middleware worry about sending them. There is a default error handler built into Express. To use it, you need to call the next function with an argument (other than the string ‘route’).
1 2 3 4 5 6 7 8 9 10 11 |
private getPostById = (request: express.Request, response: express.Response, next: NextFunction) => { const id = request.params.id; this.post.findById(id) .then((post) => { if (post) { response.send(post); } else { next('Post not found'); } }); } |
Handling the error in this way results in a 500 Internal Server Error and the error page is rendered.
If we want to handle it differently, we need to create our Express error handling middleware
Express Error handling middleware
First, let’s create a class that we are going to use to throw errors.
src/exceptions/HttpException.ts
1 2 3 4 5 6 7 8 9 10 11 |
class HttpException extends Error { status: number; message: string; constructor(status: number, message: string) { super(message); this.status = status; this.message = message; } } export default HttpException; |
If you would like to know more about the Error object and the errors in general, check out Handling errors in JavaScript with try…catch and finally
Defining Express error handling middleware is almost the same as any other middleware, except we use four arguments instead of three, with the error being the additional first argument.
src/middleware/error.middleware.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
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.
The last thing to do is to attach the error handling middleware to our app:
src/app.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
import * as bodyParser from 'body-parser'; import * as 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, } = process.env; mongoose.connect(`mongodb://${MONGO_USER}:${MONGO_PASSWORD}${MONGO_PATH}`); } } export default App; |
1 2 3 4 5 6 7 8 9 10 11 |
private getPostById = (request: express.Request, response: express.Response, next: express.NextFunction) => { const id = request.params.id; this.post.findById(id) .then((post) => { if (post) { response.send(post); } else { next(new HttpException(404, 'Post not found')); } }); } |
We can use the HttpException in the same manner when a post that the user wants to delete or modify a post that doesn’t exist. That would mean creating the HttpException in the same way. To avoid redundant code, we can prepare an exception just for that situation.
src/exceptions/PostNotFoundException.ts
1 2 3 4 5 6 7 8 9 |
import HttpException from "./HttpException"; class PostNotFoundException extends HttpException { constructor(id: string) { super(404, `Post with id ${id} not found`); } } export default PostNotFoundException; |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
private getPostById = (request: express.Request, response: express.Response, next: express.NextFunction) => { const id = request.params.id; this.post.findById(id) .then((post) => { if (post) { response.send(post); } else { next(new PostNotFoundException(id)); } }); } private modifyPost = (request: express.Request, response: express.Response, next: express.NextFunction) => { const id = request.params.id; const postData: Post = request.body; this.post.findByIdAndUpdate(id, postData, { new: true }) .then((post) => { if(post) { response.send(post); } else { next(new PostNotFoundException(id)); } }); } private deletePost = (request: express.Request, response: express.Response, next: express.NextFunction) => { const id = request.params.id; this.post.findByIdAndDelete(id) .then((successResponse) => { if (successResponse) { response.send(200); } else { next(new PostNotFoundException(id)); } }); } |
Validating incoming data
Another thing worth mentioning is validating input data to prevent the users from creating invalid documents in our collections. To do that I use a package called class-validator with an additional middleware.
The first thing to do is to create a data transfer object (DTO) file that carries data between our functions. It contains specification on how should the incoming data look.
src/posts/post.dto.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
import { IsString } from 'class-validator'; class CreatePostDto { @IsString() public author: string; @IsString() public content: string; @IsString() public title: string; } export default CreatePostDto; |
To use decorators with TypeScript, you need to add "experimentalDecorators": true to your tsconfig.json
When we got that down, the only thing left is the validation middleware. Since the body of our request is a plain object, we need to transform it into our class first. To do that, I use the class-transformer package.
src/middleware/validation.middleware.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
import { plainToClass } from 'class-transformer'; import { validate, ValidationError } from 'class-validator'; import * as 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) => Object.values(error.constraints)).join(', '); next(new HttpException(400, message)); } else { next(); } }); }; } export default validationMiddleware; |
The class-validator package validates the object, and if it finds some errors, the middleware calls the next function with the error details. Since we pass an error into the next function, the Express error middleware that we described above takes care of it. The errors variable keeps an array of errors, each of them having the constraints object with the details. This simple example creates a string of all of the issues.
The 400 Bad Request status code means that there is something wrong with the request that the client sent.
If you need to, you can also pass an array so that it can be easier to iterate over on the frontend
A thing left to do is to attach the middleware:
1 2 3 4 5 6 |
import validationMiddleware from '../middleware/validation.middleware'; import CreatePostDto from './post.dto'; private initializeRoutes() { this.router.post(this.path, validationMiddleware(CreatePostDto), this.createPost); } |
Since we want it only on some of our endpoints, we attach it straight before the handler functions. In the example above, the middleware validates the data before the this.createPost function runs.
Validating the PATCH handler data
It would be great to use that validation in our updating logic too. There is a small catch: in our CreatePostDto class, all fields are required, and we are using HTTP PATCH that allows for updating just some of the properties without passing the rest of them. There is an easy solution for that thanks to the skipMissingProperties option.
src/middleware/validation.middleware.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 |
function validationMiddleware<T>(type: any, skipMissingProperties = false): express.RequestHandler { return (req, res, next) => { validate(plainToClass(type, req.body), { skipMissingProperties }) .then((errors: ValidationError[]) => { if (errors.length > 0) { const message = errors.map((error: ValidationError) => Object.values(error.constraints)).join(', '); next(new HttpException(400, message)); } else { next(); } }); }; } |
It skips validating all the properties that are missing, so if you are updating just a part of the document, it doesn’t cause an error just because a required property is not present.
1 2 3 4 |
private initializeRoutes() { this.router.patch(`${this.path}/:id`, validationMiddleware(CreatePostDto, true), this.modifyPost); this.router.post(this.path, validationMiddleware(CreatePostDto), this.createPost); } |
Summary
In this article, we’ve covered Typescript Express error handling. To do that we’ve created an Express error handling middleware and used the next function with an additional argument to pass the error to the error handling middleware. Aside from that, we’ve learned how to validate incoming data in our POST and PATCH handlers using the class-validator package. All those new skills will surely be useful, especially because the next part of the course will cover registering users and authentication.
I’m having a problem with CreatePostDto class because it does not have a constructor:
return new TSError(diagnosticText, diagnosticCodes)
^
TSError: ⨯ Unable to compile TypeScript:
src/posts/post.dto.ts(5,9): error TS2564: Property ‘author’ has no initializer and is not definitely assigned in the constructor.
src/posts/post.dto.ts(8,9): error TS2564: Property ‘content’ has no initializer and is not definitely assigned in the constructor.
src/posts/post.dto.ts(11,9): error TS2564: Property ‘title’ has no initializer and is not definitely assigned in the constructor.
Only when I add a constructor with all 3 attributes, then it compiles…
Is there a way to not require the constructor?
I haven’t experienced that error, but I’ve looked it up and you can add strictPropertyInitialization flag. It can be done either in the terminal, or within the compiler options.
You can also use the ! modifier.
For detailed instructions check out the TypeScript 2.7 Release Notes.
Hi i am new with this. How can i return json message when validate incoming data. Thank you
Hi.
The findById not found (404) is not working for me.
I’ve tried running the GIT version (branch 3) with the same result:
(node:28712) UnhandledPromiseRejectionWarning: CastError: Cast to ObjectId failed for value “5cca196d241e220934ad4e72a” at path “_id” for model “Post”
Hi Marcin, I’ve facing Authentication token missing issues while inserting data. Please can you let me know what is the issue
class-transformer was throwing some errors[1]
I had to import ‘reflect-metadata’;
resolution in validation.middleware.ts
import ‘reflect-metadata’;
import { plainToClass } from ‘class-transformer’;
import { validate, ValidationError } from ‘class-validator’;
[1] Error: Uncaught TypeError: Reflect.getMetadata is not a function
thanks for the tutorial! it’s really helpful
Hi, thanks for the article. However, you don’t mention how to really handle Mongoose errors in a typed environment. The HttpException is explicitly created only for the most obvious cases and its interface does not expect any programming errors such as CastError. Do you know a clean and efficient way to prepare your application for such errors?
Cheers.
when used PostNotFoundException and I recieved this error:
(node:17496) [DEP0005] DeprecationWarning: Buffer() is deprecated due to security and usability issues. Please use the Buffer.alloc(), Buffer.allocUnsafe(), or Buffer.from() methods instead.
(Use
node --trace-deprecation ...
to show where the warning was created)Currently, I using node v14.15.0 and npm 6.14.8 (latest)
Please, tell me why?
Probably one of the dependencies uses
new Buffer(number)
instead of
Buffer.alloc(number)
The first thing I would try would be to update the dependencies in your package.json
it’s so nice to find this tutorial. it’s helped me a lot. thank you. And Do you want to write a post about the class-validator and class-transform? That part is hard for me. hahaha
How can it do the transform and validate? it’s a magic。
thanks