Introduction
How we present errors and response messages to the client significantly impacts how easily they can handle and act on them. In a microservice architecture where different stacks are used for backend services, a consistent error response structure becomes essential—especially when all endpoints are consumed by a single frontend application.
Uniform error formatting allows frontend developers to implement a single, reliable error-handling strategy.
In this article, I'll show you how to build a standardized error response structure using TypeScript, Node.js, and Express.js.
Error Response Format
All error responses will follow this structure:
{
"errors": [
{
"message": "Description",
"field": "Referenced field, e.g., Email"
},
{
"message": "Description",
"field": "Referenced field, e.g., Name"
}
]
}
Initializing the Project
1. Create a new directory and initialize a Node project
mkdir error-handling-example
cd error-handling-example
npm init -y
2. Install necessary dependencies
# TypeScript and linting
npm install -D typescript tslint
# Express and related packages
npm install express express-validator
npm install -D @types/express express-async-errors ts-node-dev
3. Initialize TypeScript configuration
tsc --init
This will generate a tsconfig.json
file.
Project Structure
Create a src/
directory and open your project in your preferred IDE. Inside src/
, organize files as follows:
src/
├── app.ts
├── routes/
│ └── routes.ts
├── controllers/
│ └── signup.ts
├── errors/
│ ├── custom-error.ts
│ └── request-validation-error.ts
├── middlewares/
│ └── error-handler.ts
Creating the Server
In src/app.ts
:
import express from "express";
import "express-async-errors";
import authentication from "./routes/routes";
import { NotFoundError } from "./errors/not-found-err";
import { errorHandler } from "./middlewares/error-handler";
const app = express();
app.use(express.json());
app.use("/api/users", authentication);
app.all("*", async () => {
throw new NotFoundError();
});
app.use(errorHandler);
const port = 3000;
app.listen(port, () => {
console.log(`Server is listening on port ${port}`);
});
express-async-errors
allows us to throw errors in async routes without wrapping them intry-catch
.
Setting Up the Route
In src/routes/routes.ts
:
import express from "express";
import { body } from "express-validator";
import { signUp } from "../controllers/signup";
const router = express.Router();
router.post(
"/signup",
[
body("email").isEmail().withMessage("Invalid email"),
body("password")
.trim()
.isLength({ min: 6, max: 20 })
.withMessage("Password must be between 6 to 20 characters"),
],
signUp
);
export default router;
Creating the Controller
In src/controllers/signup.ts
:
import { Request, Response } from "express";
import { validationResult } from "express-validator";
import { RequestValidationError } from "../errors/request-validation-error";
export const signUp = async (req: Request, res: Response) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
throw new RequestValidationError(errors.array());
}
const { email, password } = req.body;
// Add database operations or token logic here
res.status(201).send({ email, password });
};
Creating a Custom Error Class
In src/errors/custom-error.ts
:
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 }[];
}
Handling Validation Errors
In src/errors/request-validation-error.ts
:
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 => ({
message: err.msg,
field: err.param,
}));
}
}
Creating the Error Handler Middleware
In src/middlewares/error-handler.ts
:
import { Request, Response, NextFunction } from "express";
import { CustomError } from "../errors/custom-error";
export const errorHandler = (
err: Error,
req: Request,
res: Response,
next: NextFunction
) => {
if (err instanceof CustomError) {
return res.status(err.statusCode).send({ errors: err.serializeErrors() });
}
console.error(err);
res.status(400).send({
errors: [{ message: "Something went wrong" }],
});
};
You can extract the validation logic from the controller into a reusable middleware for better reusability.
Running the Server
In your package.json
, add a start script:
"scripts": {
"start": "ts-node-dev src/app.ts"
}
Then run:
npm start
Your server will start at http://localhost:3000.
Use tools like Postman or Insomnia to test your API.
Final Note
This is my first article here. I would love your feedback!
Feel free to leave comments or suggestions for improvements.
Top comments (0)