Part 1: Refactoring Validation Middleware
In your previous assignments (contact form, registration, and login), you placed validation middleware directly in controller files alongside route handlers. This approach kept related code together and simplified learning the concepts. For small applications with just a few forms, this organization works perfectly well and remains a valid choice in professional development.
However, as applications grow and teams expand, maintaining strict separation of concerns becomes increasingly valuable. Middleware serves a distinct architectural purpose separate from business logic, and organizing it accordingly makes codebases easier to navigate, test, and maintain. This refactoring teaches you to recognize when architectural patterns should evolve to support application growth.
Understanding the Trade-offs
Before refactoring, you should understand why both organizational approaches exist in professional codebases and when each makes sense.
Current Approach: Validation with Controllers
Your current structure keeps validation rules in controller files:
src/
└── controllers/
└── forms/
├── contact.js # route handlers + validation
├── registration.js # route handlers + validation
└── login.js # route handlers + validation
Advantages: Everything related to a single feature lives together. When working on the contact form, you have handlers and validation in one file. This reduces context switching and makes the codebase feel cohesive. For applications with fewer than 10-15 forms, this approach remains practical and maintainable.
Disadvantages: Controller files grow larger as validation becomes more complex. The architectural boundary between middleware and controllers blurs, making it harder to understand each layer's responsibilities. Testing becomes more complicated because validation logic is mixed with business logic.
Refactored Approach: Dedicated Validation Middleware
The refactored structure separates validation into its own middleware directory:
src/
├── controllers/
│ └── forms/
│ ├── contact.js # only route handlers
│ ├── registration.js # only route handlers
│ └── login.js # only route handlers
└── middleware/
├── auth.js
└── validation/
└── forms.js # all form validation
Advantages: Clear architectural boundaries make the codebase easier to understand. Middleware responsibilities are explicit and separated from business logic. Testing becomes simpler because each layer can be tested independently. As the application grows, this organization scales naturally. New team members can locate validation logic predictably.
Disadvantages: More files and directories to navigate. Related code is separated across different locations. For very small applications, this structure might feel like unnecessary overhead.
In larger applications with dozens of forms, you would create separate validation files for each form: contact.js, registration.js, login.js, and so on. Since your application currently has only a few forms, you will consolidate all validation rules into a single forms.js file. This balances architectural clarity with practical simplicity.
When to Use Each Approach
Choose validation with controllers when building prototypes, small internal tools, or applications with fewer than 10 forms. The simplicity and cohesion outweigh architectural concerns.
Choose dedicated validation middleware for applications expected to grow, codebases with multiple developers, or when strict architectural patterns improve maintainability. The organizational overhead pays dividends as complexity increases.
Your university information system has grown to include multiple forms and will continue expanding. This makes it an ideal candidate for refactoring to the dedicated middleware approach. You are making this change not because the current approach is wrong, but because you are preparing the codebase for future growth.
Understanding Validation Middleware Structure
Before you begin refactoring, you need to understand what validation middleware actually is and how it works with Express.
Express middleware functions are functions that have access to the request object (req), the response object (res), and the next middleware function in the application's request-response cycle (next). When you use express-validator, you create validation middleware by calling functions like body() that return middleware functions.
In your controller files, you have arrays of validation rules like this:
const contactValidation = [
body('subject')
.trim()
.isLength({ min: 5 })
.withMessage('Subject must be at least 5 characters long'),
body('message')
.trim()
.isLength({ min: 10 })
.withMessage('Message must be at least 10 characters long')
];
Each call to body() returns a middleware function. The array itself is also middleware that Express can use. When you pass this array to a route in routes.js, Express runs each validation function in order before calling your route handler.
Your refactoring task is to move these validation arrays out of the controller files and into a dedicated middleware file, then update your imports so the routes can still access them.
Refactoring Instructions
1. Create the Validation Middleware Directory and File
Create the directory structure src/middleware/validation/ and create a new file named forms.js inside it. This file will contain all validation middleware for your form-based routes.
At the top of forms.js, import the body function from express-validator:
import { body } from 'express-validator';
2. Move Validation Rules to the New File
Open each of your three controller files: contact.js, registration.js, and login.js. Each file contains one or more validation arrays (for example, contactValidation, registrationValidation, loginValidation, and updateAccountValidation).
Copy all of these validation arrays from the controller files into your new forms.js file. Make sure to preserve the exact variable names because your routes file already imports these specific names.
Pay careful attention to validation arrays that use custom validation functions. For example, the registration validation includes custom validators that compare confirmEmail with email and confirmPassword with password. These use req.body to access other form fields, so make sure you copy the complete validation logic including all .custom() methods.
3. Export the Validation Rules
At the bottom of forms.js, export all four validation arrays as named exports:
export {
contactValidation,
registrationValidation,
loginValidation,
updateAccountValidation
};
Using named exports allows you to import specific validation rules by name in your routes file, making the imports clear and explicit.
4. Clean Up Controller Files
Now that validation rules live in forms.js, you need to remove them from the controller files.
In each controller file (contact.js, registration.js, and login.js):
-
Remove the import statement that brings in
bodyfrom express-validator (you no longer need it in the controller) -
Remove all validation array definitions (like
contactValidation,registrationValidation, etc.) - Remove the validation array names from the export statement at the bottom of the file
After cleanup, each controller file should only import validationResult from express-validator (because the route handlers still use it to check for validation errors), and should only export the route handler functions.
For example, contact.js should import validationResult at the top and export only showContactForm, processContactForm, and showContactResponses at the bottom. The contactValidation array should be completely gone from this file.
5. Update Route Imports
Open src/controllers/routes.js. This file currently imports validation rules and route handlers together from each controller. You need to split these imports so validation rules come from the new middleware file and route handlers continue coming from controllers:
- Remove validation imports from controller import statements.
-
Add a new import statement to bring in all the validation rules from
forms.js.
Pay close attention to relative paths. Controller imports use ./forms/ because routes.js and the forms directory are both in the controllers directory. Middleware imports use ../middleware/ because you need to go up one level from controllers to reach middleware.
6. Test the Refactoring
Restart your server and systematically test each form to verify the refactoring worked correctly:
- Visit http://127.0.0.1:3000/contact and submit the form with valid data. Verify it saves correctly and shows a success message.
- Submit the contact form with invalid data (empty subject, message too short). Verify that validation error messages appear.
- Test the registration form at http://127.0.0.1:3000/register with both valid and invalid data (mismatched emails, weak passwords, etc.).
- Test the login form at http://127.0.0.1:3000/login with correct credentials, incorrect credentials, and missing fields.
- If you are logged in as a user, test the account edit functionality at http://127.0.0.1:3000/users by clicking edit on your account.
If any validation fails to work or you see errors about missing modules, check your work carefully:
-
Verify that
forms.jsexports all four validation arrays - Confirm that controller files no longer define or export validation arrays
-
Double-check import paths in
routes.js(especially the../in the middleware import) - Make sure you did not accidentally change any validation logic when moving it
Good refactoring maintains identical external behavior while improving internal structure. After refactoring, your forms should work exactly as they did before. If behavior changes or validation stops working, you introduced an error during the refactoring process.
Professional developers test thoroughly after refactoring. Work methodically: move one validation array, test it, then move the next. This incremental approach makes it easier to identify and fix problems quickly.