Refactoring Your Node.js API: Why Your Routes Should Not Be Controllers
For two years, I allowed business logic to reside within my route files in a Node.js API. This approach cluttered my route definitions and made it challenging to manage and scale the application effectively. In this post, I will share how I refactored my Node.js API by introducing a proper controller layer and migrating the business logic out of the routes.
The Problem with Business Logic in Routes
Embedding business logic in route handlers can lead to several issues, including:
- Maintainability: As the application grows, routes become unwieldy and difficult to manage.
- Testing: Testing routes with embedded logic is cumbersome and can lead to fragile tests.
- Separation of Concerns: Mixing routing logic with business logic violates the principle of separation of concerns, complicating the understanding of the application's structure.
Benefits of Separating Routes and Controllers
Separating routes from controllers offers several advantages:
- Improved Readability: With a clear distinction between routing and business logic, the code is easier to read and understand.
- Enhanced Testability: Isolated controller functions can be tested independently, leading to more robust and reliable tests.
- Easier Maintenance: Modifications to business logic can be made in the controller files without affecting the routing structure.
Refactoring the Architecture
To address the issues identified, I migrated the business logic from route files into dedicated controller files. The goal was to ensure that route files only handle HTTP requests and delegate the actual processing to controller methods.
Migration Process
The migration process involved the following steps:
- Identify Route Handlers: Review route files to find handlers containing inline logic.
- Create Controller Files: For each route file, create a corresponding controller file if it doesn't already exist.
- Move Logic to Controllers: Transfer the logic from route handlers to appropriately named functions in the controller files.
- Update Route Files: Replace inline handlers in route files with references to the corresponding controller functions.
- Testing: After migration, manually test all endpoints to ensure functionality remains intact.
Example Migration
Here's a detailed look at the migration process:
Before Migration (auth.routes.ts)
router.post('/signin', async (request: Request, response: Response) => {
const { email, password } = request.body;
// Logic for signing in
});
After Migration (auth.routes.ts)
import { authController } from '../controllers/auth.controller';
router.post('/signin', authController.signIn);
Controller File (auth.controller.ts)
import { Request, Response } from 'express';
import authService from '../services/auth.service';
import { STATUS } from '../util/status.util';
export async function signIn(req: Request, res: Response) {
const { email, password } = req.body;
if (!email || !password) {
return res.status(STATUS.BAD_REQUEST).send({ error: 'Invalid login' });
}
try {
const user = await authService.signIn(email, password);
return res.send({ data: user });
} catch (e) {
return res.status(STATUS.BAD_REQUEST).send({ error: 'Invalid login' });
}
}
Project Structure
After completing the migration of all route files to controllers, the project structure now looks like this:
Route Files (src/routes)
audio.routes.tsauth.routes.tsbugreport.routes.tscards.routes.tscategory.routes.tsdeck.routes.tshealthcheck.routes.tsinternal.routes.ts
Controller Files (src/controllers)
audio.controller.tsauth.controller.tsbugreport.controller.tscards.controller.tscategory.controller.tsdeck.controller.tsinternal.controller.ts
Common Pitfalls to Avoid
When refactoring your route files, be aware of these common pitfalls:
- Neglecting Testing: Always ensure that you test all endpoints after refactoring to catch any potential issues.
- Inconsistent Naming Conventions: Maintain a consistent naming convention for your controller and service files to enhance clarity.
- Skipping Documentation: Update any documentation to reflect the new structure, making it easier for your team to understand the changes.
Conclusion
Refactoring my Node.js API to separate routes from controllers has significantly improved maintainability and readability. The new structure adheres to best practices, making it easier to test and scale the application. If you find yourself with convoluted route files packed with business logic, consider applying a similar refactoring strategy. Your code—and your future self—will thank you!
Best Practices for Refactoring Route Files
When refactoring your route files, keep these best practices in mind:
- Adhere to the principle of separation of concerns by keeping your routing logic and business logic in separate files.
- Maintain a consistent naming convention for your controller and service files to enhance clarity.
- Regularly review and refactor your codebase to prevent the accumulation of inline logic in your routes.
- Implement thorough testing after refactoring to ensure all functionality remains intact.