Introduction
In this series we will setup an express server using Typescript
, we will be using TypeOrm
as our ORM for querying a PostgresSql Database, we will also use Jest
and SuperTest
for testing. The goal of this series is not to create a full-fledged node backend but to setup an express starter project using typescript which can be used as a starting point if you want to develop a node backend using express and typescript.
Overview
This series is not recommended for beginners some familiarity and experience working with nodejs
, express
, typescript
and typeorm
is expected. In this post which is part five of our series we will set up : -
- Set up a global error handler.
- Implement express async middleware library on our own.
- Remove try / catch blocks from the controller.
Step One: Express Global Handler
In this section we will setup 2 handlers, one for handling all our global errors and the next for handling 404 not found routes.
Before doing so lets create some error classes. Under the src/utils
folder create a new file called BaseError.ts
-
export class BaseError extends Error {
constructor(message: string) {
super(message);
this.name = this.constructor.name;
}
}
Similarly lets create another file under src/utils
called NotFoundError.ts
import { BaseError } from './BaseError';
export class NotFoundError extends BaseError {
status: number;
constructor(message: string, status: number) {
super(message);
this.status = status;
}
}
If you are unfamiliar with creating custom error classes I would recommend you read - https://javascript.info/custom-errors.
Now under server.ts
lets add the following 2 functions:
private globalNotFoundHandler() {
this.app.use((req, res, next) => {
const error = new NotFoundError('Resource not found', 404);
next(error);
});
}
private globalErrorHandler() {
this.app.use(
(error: Error, req: Request, res: Response, next: NextFunction) => {
console.log('Error (Global Error Handler)', error.stack);
if (error instanceof NotFoundError) {
return res.status(error.status).json({
status: false,
statusCode: error.status,
message: error.message,
});
}
// Handling internal server errors
return res.status(500).json({
status: false,
statusCode: 500,
message: 'Something unusual Happened',
});
}
);
}
In the above code we added a handler for handling 404 not found routes and a global error handler. In express if your middleware takes in 4 arguments namely error, req, res, next
it is considered as an error handler middleware. You can see in the NotFoundError
handler we used next(error)
meaning we passed the error to our globalErrorHandler. We can also invoke our globalErrorHandler by throwing an error
from our middlewares, controllers all throw error
calls will land in globalErrorhandler.
Now lets call these handlers in our constructor like so -
constructor() {
// Initialize express application
this.app = express();
// add express middlewares
this.addMiddlewares();
// add our routes
this.addRoutes();
// handle 404 not found routes
this.globalNotFoundHandler();
// handle all global errors
this.globalErrorHandler();
}
Make sure your 404 handler is called after your routes and global error handler is called last.
Step Two: Setup Async Handler Middleware
We will be using express-async-handler
library for this. But instead on installing it, lets read their code on github, understand and implement it. Before that lets understand what a middleware is in express. Middlewares are like the layers of an onion in order to reach to the last layer you need to go through all the layers.
In the following code our last layer is our controller (which is the last piece of middleware). Before reaching the controller our request will go through all these middleware functions -
this.router.get(`/`, authMiddleware, validationMiddleware, todoController.createTodo)
App wide our last layer (middleware) is our globalErrorhandler
function.
Cool now how we write middlewares in express, a middleware is a function that receives req, res, next
as arguments like -
const authMiddleware = (req, res, next) => {}
To go from one layer to another layer express uses the next() function.
Now we all are familiar with currying in javascript. A curried function is a function that returns another function
-
const curriedFunction = () => () => {}
Now if I had to write my middleware in this fashion it would be -
const curriedMiddleware = () => (req, res, next) => {}
And if I had to use this curriedMiddleware
I will use it like so -
this.router.get(`/`, authMiddleware(), todoController.createTodo)
This is the same pattern that express-async-middleware
library uses. With this pattern : -
- We can pass our controller function to the async middleware.
- The async middleware will resolve our controller function catch any errors and if we get any error it will call
next(error)
. -
next(error)
will end up in our globalErrorHandler and will gracefully handle all the internal server errors.
Now in our src
folder create a new folder middlewares
under it create a new file called asyncHandler.ts
-
import { Request, Response, NextFunction } from 'express';
export function asyncHandler(controllerFunction: any) {
return function asyncMiddleware(
req: Request,
res: Response,
next: NextFunction
) {
const controllerPromise = controllerFunction(req, res, next);
return Promise.resolve(controllerPromise).catch(next);
};
}
This is the same code as the express-async-handler library:
- First we have our curried function
asyncHandler
that accepts our controller function as an argument and returns the middleware functionasyncMiddleware
. - In our middleware function we get
req, res, next
arguments. Then we pass thereq, res, next
to our controller function after all we use them in our controllers. -
const controllerPromise = controllerFunction(req, res, next);
here we are executing our controller function, which will return a promise if our controller isasync
or will just return whatever our controller returns. - Finally we resolve our
controllerPromise
variable and if we catch any error we just callnext(error)
and this lands in our globalErrorHandler.
Step 3: Using async handler
Now lets use our async handler in our todos.router.ts
we do -
addRoutes(): void {
this.router.get('/', asyncHandler(todosController.getAllTodos));
this.router.post('/', asyncHandler(todosController.createTodo));
this.router.get('/:todoId', asyncHandler(todosController.getTodoById));
}
We will pass our controller to the asyncHandler curried middleware. Lets remove all the try-catch blocks from our controller functions -
import { Request, Response } from 'express';
import { todosService } from './todos.service';
class TodoController {
async getAllTodos(req: Request, res: Response) {
const todos = await todosService.getAllTodos();
return res.status(200).json({
status: true,
statusCode: 200,
todos,
});
}
async getTodoById(req: Request, res: Response) {
const { todoId } = req.params;
const todo = await todosService.getTodoById(todoId);
if (!todo) {
return res.status(404).json({
status: false,
statusCode: 404,
message: `todo not found for id - ${todoId}`,
});
}
return res.status(200).json({
status: true,
statusCode: 200,
todo,
});
}
async createTodo(req: Request, res: Response) {
const { text, status } = req.body;
const newTodo = await todosService.createTodo(text, status);
return res.status(201).json({
status: true,
statusCode: 201,
todo: newTodo.raw,
});
}
}
export const todosController = new TodoController();
Look how lean and clean our controller functions are now. Lets test our endpoints whether everything is working fine by running npm run dev
. To test the 404 not found handler visit a route which does not exists on your server. To test the global error Handler pass a json request which does not have the text field
to create todo endpoint, TodoEntity will throw an error.
Overview
In this tutorial instead of pulling in the library we implemented it on our own by reading its github code. I encourage all the developers to read open source code it is very easy, it requires some time and attention but very helpful. All the code for this tutorial can be found under the feat/async-handler
branch here. In the next tutorial we will setup request validation using zod and implement another library on our own until next time PEACE.
Top comments (0)