As we build complex web applications with an increasing number of routes and endpoints, managing request validation becomes a critical aspect of the API design. In Node.js applications, employing a clean and efficient validation strategy for every request reaching the server is pivotal. This article proposes an effective approach to API request validation where every route has its own validation schema. This technique leverages the lightweight JavaScript library Yup
for schema building and validation, and introduces a middleware function for validating request bodies.
The primary benefits of this approach include:
Streamlined validation with clearly defined schemas for each route.
Utilization of Yup's easy-to-use API and human-readable error messages.
Centralized request validation middleware simplifies code organization and debugging.
Decoupling of validation logic from route handlers, promoting code reuse and separation of concerns.
Increased application reliability by ensuring all incoming requests are valid.
Simple human-readable errors that the frontend can show the user directly/
Implementing the System
Let's dive into how we can create this system.
1. Route-Specific Validation Schemas
Firstly, every route should have its validation schemas defined in a validators
file.
This one is called expenses.validator.ts
Here's an example of a validation schema for an expense route using Yup:
import * as Yup from "yup";
export const addExpenseValidationSchema = Yup.object({
price: Yup.string()
.required()
.matches(/^\d+(\.\d+)?$/, "Enter a valid price")
.label("Price"),
category: Yup.string().required("Please select a category").label("Category"),
remark: Yup.string().label("Remark").required(),
});
2. Route-Schema Lookup Table
Next, we establish a schema lookup table defined in a schema-lookup
file which maps routes to their corresponding validation schemas. This serves as a centralized place for all our route-specific schemas:
import { addExpenseValidationSchema } from "../modules/expense/expense.validators";
const schemaLookup: { [key: string]: any } = {
"POST/expenses": addExpenseValidationSchema,
};
export default schemaLookup;
3. Request Validation Middleware
With our schemas and lookup table set up, we create a middleware function, bodyValidator
, which uses the request's method and path to fetch the correct schema from the lookup table:
import { NextFunction, Request, Response } from "express";
import { BadRequestError } from "../config/errors";
import schemaLookup from "./schema-lookup";
export async function bodyValidator(
req: Request,
res: Response,
next: NextFunction
) {
const route = `${req.method}${req.path}`;
const schema = schemaLookup[route];
if (!schema) return next();
try {
await schema.validate(req.body);
next();
} catch (error) {
throw new BadRequestError(error.errors[0]);
}
}
4. Integrating the Middleware
Finally, we wire up our application to use the bodyValidator
middleware. This ensures all incoming requests pass through our validation process:
app.use("/api/v1", bodyValidator, routes);
5. Routes Without Repeated Validation Middleware
With the validation system, we've described above in place, our routes file becomes remarkably clean and simple. Here's an example of what a routes file for our expenses endpoint would look like:
Before
const router = require("express").Router();
import { bodyValidator } from "../../../src/middlewares/validator";
import controller from "../expense/expense.controller";
router.post("/expenses", bodyValidator, controller.create);
router.put("/expenses", bodyValidator, controller.update);
router.patch("/expenses", bodyValidator, controller.singleUpdate);
export default router;
After
const router = require("express").Router();
import controller from "../expense/expense.controller";
router.post("/expenses", controller.create);
router.put("/expenses", controller.update);
router.patch("/expenses", controller.singleUpdate);
export default router;
Notice how there's no longer a need to include the bodyValidator
middleware within each route? This not only enhances code readability but also improves maintainability by keeping your code DRY (Don't Repeat Yourself). All the magic of validation is happening in the background, managed centrally by the bodyValidator
middleware.
6. Conclusion
This approach allows for a more structured and elegant way of handling API request validation. It demonstrates the power of middleware in Express.js, allowing us to write cleaner, more maintainable code. By leveraging the flexibility of Yup for schema definition and validation, we've streamlined the process of ensuring all incoming requests are valid and structured as expected, leading to more robust and reliable APIs.