9. The Controller Pattern (Organizing Your Logic) ποΈ
As you add more routes and logic to your application, your router files can become large and difficult to read. The Controller Pattern is a way to solve this by separating your route handling logic from your route definitions.
- Router: Its only job is to define the URL paths and HTTP methods and direct incoming requests to the correct function. Itβs the βtraffic cop.β
- Controller: A separate module that contains the actual functions (the business logic) that are executed when a route is matched. It handles the work of interacting with models, processing data, and sending the response. Itβs the βspecialistβ who does the work.
Analogy: The Router is like a restaurantβs menu. It lists the available dishes and their prices (the routes). The Controller is the chef in the kitchen who knows the recipe for each dish and actually prepares it when an order comes in.
This separation makes your code cleaner, more organized, easier to test, and more maintainable.
The Workflow: From Route to Controller
- A request comes in to your Express app.
- It matches a route defined in a router file.
- The router file calls the corresponding handler function, which it has imported from a controller file.
- The controller function executes all the business logic.
- The controller function sends the response back to the client.
Example: Refactoring to Use Controllers
This pattern involves structuring your code across at least three files.
Step 1: Create the Controller File
This file will contain all the logic for handling requests. You define and export each handler function (e.g., getAllProducts, getProductById).
// In a real app, this data would come from a database model.
let products = [{ id: 1, name: 'Laptop' }, { id: 2, name: 'Keyboard' }];
// Define and export each handler function.
exports.getAllProducts = (req, res) => {
res.json(products);
};
exports.getProductById = (req, res) => {
const product = products.find(p => p.id === parseInt(req.params.id));
if (!product) {
return res.status(404).send('Product not found');
}
res.json(product);
};
exports.createProduct = (req, res) => {
const newProduct = { id: products.length + 1, name: req.body.name };
products.push(newProduct);
res.status(201).json(newProduct);
};Step 2: Update the Router File
The router file becomes much cleaner. Its only job is to import the controller functions and map the routes to them (e.g., router.get('/', productController.getAllProducts)).
const express = require('express');
const router = express.Router();
// 1. Import the controller functions
const productController = require('../controllers/productController');
// 2. Map routes to controller functions
router.get('/', productController.getAllProducts);
router.get('/:id', productController.getProductById);
router.post('/', productController.createProduct);
module.exports = router;Step 3: The Main App File
Your main app.js file doesnβt need to change at all. It simply imports and mounts the router, remaining unaware of the controllerβs implementation details.
const express = require('express');
const app = express();
const productRoutes = require('./routes/productRoutes');
app.use(express.json());
// Mount the router
app.use('/products', productRoutes);
app.listen(3000, () => console.log('Server is running on port 3000'));β¨ Summary
- The Controller pattern is a way to separate concerns in your Express application.
- Routers are responsible for defining the URL paths and HTTP methods.
- Controllers are responsible for the business logic that executes for those routes.
- This pattern leads to code that is cleaner, more organized, and easier to test and maintain.
- The typical flow is:
app.jsβRouter FileβController File.