
Hi everyone! I hope you’re all doing fine. My name is Julian, and today we’re going to learn about controllers. Before diving in, let me share an experience that perfectly illustrates why controllers are essential.
Jimmy’s Story: The Need for Controllers
You might remember Jimmy from our earlier discussion on routers.
Jimmy’s café was doing great. Initially, when he had just a few orders, everything was simple.
Jimmy himself would take the orders, process them, and pass them to the kitchen. His workers (routers) would just direct the orders to the right places, and things ran smoothly. But as Jimmy’s café grew, so did the complexity of managing his orders.
He started to have more customers with special requests—like those wanting gluten-free or vegan meals. The kitchen staff began to struggle with understanding the details of each order, and customers started getting frustrated when their meals weren’t exactly as requested.
Jimmy realized that just passing the orders along wasn’t enough.
He needed someone to oversee the process, ensuring that the orders were processed correctly, all customer requirements were met, and nothing was overlooked.
So, Jimmy hired a supervisor for each section of the café: one for managing the orders, one for ensuring quality control in the kitchen, and one for handling special requests.
These supervisors not only received the orders (just like the workers did) but also ensured that the orders were handled correctly and everything ran smoothly in the kitchen. These supervisors are like the controllers in Express.js.
Why Controllers Are Needed
In the same way that supervisors ensure each aspect of the café runs smoothly, controllers manage the business logic of your application. Let’s break it down:
- Separation of Concerns:
Just like Jimmy’s café divided the responsibilities between workers and supervisors, controllers help separate routing (handling incoming requests) from business logic (processing and responding to those requests). This makes your code cleaner and easier to manage. - Cleaner Code:
If Jimmy had mixed the tasks of taking orders with managing the kitchen, things would get messy. Similarly, controllers keep business logic separate from routes, making your code more readable. - Reusability:
Jimmy’s café supervisors don’t always redo tasks from scratch. They follow established procedures. Similarly, controllers allow you to reuse logic across different routes without duplication. - Error Handling:
If an order goes wrong in Jimmy’s café, the supervisor steps in to correct it. Controllers handle errors in your app and ensure that everything runs smoothly, even when something goes wrong. - Scalability:
As Jimmy’s café grew, he hired more supervisors to manage different aspects of the business. Controllers do the same—they allow your app to grow and scale without becoming unmanageable.
So let’s try to understand when to Use Controllers
You should consider using controllers when
- You have multiple routes with similar logic. If multiple routes require the same logic or tasks (like checking user authentication), controllers allow you to reuse that logic rather than duplicating it in every route handler.
- You need better organization and maintainability. As your app grows, separating logic into controllers helps you keep your code organized and more maintainable, especially if you have many routes and services interacting.
- You want to handle errors consistently. If you need a standard way to handle errors across your application (such as catching missing parameters or bad requests), controllers give you a centralized location to manage those errors.
- Your routes involve complex business logic. If you find that your routes are getting crowded with logic like data validation, processing, or interacting with other services (like databases which we will learn in upcoming videos), controllers can keep the routes clean and manageable.:
Change the implementation:
Implementing Controllers in Code
Now that we know why controllers are essential, let’s see how we can implement them in an Express.js app. Below is how the process would look, just like the supervisor in Jimmy’s café.
1. Main Application File (app.js)
This file will remain largely the same, except we’ll import the routes and their controllers.
const express = require(‘express’);
const app = express();
// Import routes
const ordersRouter = require(‘./routes/orders’);
const usersRouter = require(‘./routes/users’);
// Use routers
app.use(‘/orders’, ordersRouter);
app.use(‘/users’, usersRouter);
// Start the server
app.listen(3000, () => {
console.log(‘Server is running on port 3000’);
});
2. Orders Controller (controllers/ordersController.js)
This file will contain the logic for handling requests related to orders. This keeps the business logic separate from the routes.
// controllers/ordersController.js
// Controller to get all orders
const getAllOrders = (req, res) => {
res.send(‘Order List’);
};
// Controller to create a new order
const createOrder = (req, res) => {
res.send(‘Order Created’);
};
module.exports = {
getAllOrders,
createOrder,
};
3. Orders Routes (routes/orders.js)
Now, the routes will simply refer to the appropriate controller functions for handling requests.
javascript
Copy code
const express = require(‘express’);
const router = express.Router();
// Import controllers
const ordersController = require(‘../controllers/ordersController’);
// Define routes and delegate to controllers
router.get(‘/’, ordersController.getAllOrders); // Get all orders
router.post(‘/’, ordersController.createOrder); // Create a new order
module.exports = router;
4. Users Controller (controllers/usersController.js)
This is similar to the orders controller but handles the logic for users.
javascript
Copy code
// controllers/usersController.js
// Controller to get all users
const getAllUsers = (req, res) => {
res.send(‘User List’);
};
// Controller to create a new user
const createUser = (req, res) => {
res.send(‘User Created’);
};
module.exports = {
getAllUsers,
createUser
};
5. Users Routes (routes/users.js)
Just like the orders routes, the users routes will use the user controller to handle requests.
javascript
Copy code
const express = require(‘express’);
const router = express.Router();
// Import controllers
const usersController = require(‘../controllers/usersController’);
// Define routes and delegate to controllers
router.get(‘/’, usersController.getAllUsers); // Get all users
router.post(‘/’, usersController.createUser); // Create a new user
module.exports = router;
Summary of Changes:
- Controllers:
- controllers/ordersController.js and controllers/usersController.js are created to handle the logic for orders and users, respectively.
- Routes:
- routes/orders.js and routes/users.js now only define the route paths and delegate the logic to the appropriate controller functions.
This way, your routes and controllers are separated, making the code more modular, easier to maintain, and test.
Advantages of Using Controllers
- Better Code Organization:
Just like Jimmy organized his café by hiring supervisors, controllers organize your code by separating the business logic from the routes. - Scalability:
As Jimmy’s café grew, he added more supervisors. Similarly, as your app grows, controllers allow you to scale without adding complexity to your routes. - Consistency in Error Handling:
Jimmy’s supervisors handle all order-related issues the same way. Controllers ensure that errors are handled in a consistent manner, improving the reliability of your app. - Reusability:
Jimmy’s supervisors follow the same procedure for every order. Similarly, controllers let you reuse logic across different parts of your app. - Testability:
Just like it’s easier to evaluate a supervisor’s performance, controllers make it easier to test the individual components of your app without worrying about the route handling.
Conclusion
Controllers, like supervisors in Jimmy’s café, are vital for managing complex tasks.
They ensure that business logic, data processing, and error handling are taken care of, leaving your routes simple and focused on handling requests. By using controllers in Express.js, you can achieve a cleaner, more organized, and scalable codebase—just like Jimmy did by adding supervisors to his café.
———————
Updating the example for Code:
One example for ecommerce:
Before Using Controllers
All logic is embedded directly in the routes file:
// routes/productRoutes.js
const express = require(‘express’);
const router = express.Router();
// Fetch all products
router.get(‘/products’, (req, res) => {
res.send(“Fetching all products”);
});
// Fetch a product by ID
router.get(‘/products/:id’, (req, res) => {
res.send(`Fetching product with ID: ${req.params.id}`);
});
// Add a new product
router.post(‘/products’, (req, res) => {
res.send(“Product is created”);
});
// Update a product
router.put(‘/products/:id’, (req, res) => {
res.send(`Product with ID: ${req.params.id} is updated`);
});
// Delete a product
router.delete(‘/products/:id’, (req, res) => {
res.send(`Product with ID: ${req.params.id} is deleted`);
});
module.exports = router;
After Using Controllers
The logic is moved to a separate controller file, keeping routes clean:
Controller Code (Individual Exports)
// controllers/ProductController.js
// Fetch all products
const getAllProducts = (req, res) => {
res.send(“Fetching all products”);
};
// Fetch a product by ID
const getProductById = (req, res) => {
res.send(`Fetching product with ID: ${req.params.id}`);
};
// Add a new product
const createProduct = (req, res) => {
res.send(“Product is created”);
};
// Update a product
const updateProduct = (req, res) => {
res.send(`Product with ID: ${req.params.id} is updated`);
};
// Delete a product
const deleteProduct = (req, res) => {
res.send(`Product with ID: ${req.params.id} is deleted`);
};
// Export each function individually
module.exports = {
getAllProducts,
getProductById,
createProduct,
updateProduct,
deleteProduct,
};
Routes Code
// routes/productRoutes.js
const express = require(‘express’);
const ProductController = require(‘../controllers/ProductController’);
const router = express.Router();
router.get(‘/products’, ProductController.getAllProducts);
router.get(‘/products/:id’, ProductController.getProductById);
router.post(‘/products’, ProductController.createProduct);
router.put(‘/products/:id’, ProductController.updateProduct);
router.delete(‘/products/:id’, ProductController.deleteProduct);
module.exports = router;
Key Comparison
Before Controller:
- All route logic is handled inline, making the file lengthy and harder to maintain.
After Controller:
- Routes are simplified and focus only on mapping HTTP methods to controller methods.
- The actual logic is moved to a separate controller file, improving readability and reusability.
Key Benefits of Individual Exports
- Clarity: Named exports explicitly show what the module provides.
- Flexibility: You can import only the specific functions you need in other files.
- Testability: Individual exports make it easier to test functions separately.
- Modularity: Allows functions to be reused independently across different parts of the application.
This method works especially well in larger applications where the same controller functions might be used in different contexts or modules.