114 Building a Global Validation Middleware for Input
114 Building a Global Validation Middleware for Input
Input validation is one of the most important parts of application development, especially once an application has grown to a large scale and receives plenty of data from many different endpoints. One clean, scalable, and maintainable approach to handling input validation is to build a global validation middleware. In this article, I’ll walk through everything about building a global validation middleware—from the concept and implementation to the best practices I use in real-world projects.
Why a Global Validation Middleware?
Imagine you have 50 REST API endpoints, and each one requires different input validation. If validation is written manually inside every handler, not only does the code become messy, but it also becomes very hard to maintain and change down the road.
With a global validation middleware, we can achieve the following:
- Centralized validation: All validation logic is concentrated in a single layer.
- Reusable: Validation logic only needs to be defined once and can be used across all handlers/routes.
- Consistent error response: The error format is always consistent.
- Easier scalability: Adding or modifying validation becomes easier.
Let’s look at the flow as a diagram using mermaid code.
flowchart TD
A[Client Request] --> B(Middleware Validasi Global)
B -- Input Valid --> C{Handler}
B -- Input Tidak Valid --> D[Response: 400 Bad Request]
C --> E[Response: 200 OK]
Basic Middleware Concept
Middleware is a function that runs between an incoming request and the main handler. Middleware can inspect, modify, or halt the request flow. In some frameworks such as Express.js (Node.js), middleware is already a fundamental building block. In other backend ecosystems like NestJS, Laravel, and Go Fiber, the concept of middleware is the same.
Input Validation Middleware
The goal of validation middleware is to:
- Receive the request body/query/params.
- Validate the data against the schema rules.
- If valid, pass control to the next handler.
- If invalid, return an error response in a uniform format.
Tools: Validator & Schema
For input validation, we can use libraries such as:
- JavaScript/Node.js: Joi , Yup , Zod
- TypeScript: Zod, Yup
- Python: Pydantic, Marshmallow
- Go: go-playground/validator
In this article, I’ll use Node.js + Express + Joi because it’s the most common and straightforward, but the concept can be adapted to other languages.
Basic Implementation of a Global Validation Middleware
1. Install the Validator Library
Install Express and Joi:
1npm install express joi2. Define the Validation Schema
Create the file schemas/userSchema.js
1const Joi = require('joi');
2
3const userSchema = Joi.object({
4 name: Joi.string().min(3).required(),
5 email: Joi.string().email().required(),
6 age: Joi.number().integer().min(18).max(100).required(),
7});
8
9module.exports = {
10 userSchema
11};3. The Global Validation Middleware
Create the file middlewares/validate.js
1function validate(schema, property = 'body') {
2 return function (req, res, next) {
3 const { error } = schema.validate(req[property], { abortEarly: false });
4 if (!error) {
5 return next();
6 }
7 // API-friendly error response format
8 const details = error.details.map(err => ({
9 field: err.path.join('.'),
10 message: err.message
11 }));
12
13 return res.status(400).json({
14 code: 400,
15 status: "error",
16 message: "Input validation error",
17 details
18 });
19 };
20}
21
22module.exports = validate;4. Integration in the Route
Create the file routes/user.js
1const express = require('express');
2const { userSchema } = require('../schemas/userSchema');
3const validate = require('../middlewares/validate');
4const router = express.Router();
5
6// The validation middleware is applied globally on this route
7router.post('/', validate(userSchema), (req, res) => {
8 // If we reach this point, the input is valid
9 const user = req.body;
10 // Simulate saving the user
11 res.status(201).json({ status: "success", data: user });
12});
13
14module.exports = router;5. Set Up the Express Server
Create the file app.js
1const express = require('express');
2const userRoutes = require('./routes/user');
3const app = express();
4
5app.use(express.json());
6app.use('/users', userRoutes);
7
8app.listen(3000, () => {
9 console.log('Server jalan di http://localhost:3000');
10});Simulation: Testing the Middleware
Let’s test the POST /users API scenario.
a. Invalid Input
Send:
1{
2 "name": "B",
3 "email": "salah-email",
4 "age": 15
5}Response:
1{
2 "code": 400,
3 "status": "error",
4 "message": "Input validation error",
5 "details": [
6 { "field": "name", "message": "\"name\" length must be at least 3 characters long" },
7 { "field": "email", "message": "\"email\" must be a valid email" },
8 { "field": "age", "message": "\"age\" must be greater than or equal to 18" }
9 ]
10}b. Valid Input
Send:
1{
2 "name": "Budi",
3 "email": "budi@mail.com",
4 "age": 28
5}Response:
1{
2 "status": "success",
3 "data": {
4 "name": "Budi",
5 "email": "budi@mail.com",
6 "age": 28
7 }
8}Here’s a Coverage Simulation Table
| Endpoint | Validation Schema | Property Checked | Output When Invalid |
|---|---|---|---|
| POST /users | userSchema | body | 400 & field details |
| PUT /users/:id | userUpdateSchema | body + params.id | 400 & field details |
| POST /articles | articleSchema | body | 400 & field details |
Centralizing validation makes future scaling easier and keeps validation standards consistent.
Best Practices & Tips
- Modularize schemas: Keep a separate schema file for each resource.
- Use middleware dynamically: Middleware can be applied at the route, group, or global level whenever needed.
- Custom error formatter: Standardize the error format for frontend developers.
- Support for query & params: Extend the middleware to support validating
req.queryandreq.params. - Testing integration: Test the middleware with both unit and integration tests.
Here’s an example of extending the middleware to support body, query, and params all at once:
1function validateAll(schemas) {
2 return (req, res, next) => {
3 let allErrors = [];
4 for (const [property, schema] of Object.entries(schemas)) {
5 if (!schema) continue;
6 const { error } = schema.validate(req[property], { abortEarly: false });
7 if (error) {
8 allErrors = allErrors.concat(
9 error.details.map(err => ({
10 field: `${property}.${err.path.join('.')}`,
11 message: err.message
12 }))
13 );
14 }
15 }
16 if (allErrors.length === 0) {
17 return next();
18 }
19 return res.status(400).json({
20 code: 400,
21 status: "error",
22 message: "Input validation error",
23 details: allErrors
24 });
25 };
26}
27
28// Usage:
29// router.post('/users/:id', validateAll({
30// params: idSchema,
31// body: userSchema
32// }), (req, res) => { ... });
Closing
Input validation is the foundation of an API’s security and robustness. By building a global validation middleware, you can achieve clean code, maintainability, and consistent standardization across the entire application. Don’t let every handler rewrite its own validation—do it the way a professional engineer would: centralize it, test it, and scale it.
If you have a different approach to validation, feel free to discuss it in the comments. And remember, good middleware is middleware that won’t stress out your team six months from now!