As routes grow beyond a couple of lines each, mixing routing definitions and business logic in the same file becomes hard to maintain. This lesson introduces the controller pattern for keeping route files thin.
What Is a Controller in Express?
A controller is simply a function, or a group of exported functions, that receives (req, res, next) and contains the actual logic for handling a request. Route files import controllers and wire them to paths, rather than defining that logic inline.
Express itself has no built-in concept of a "controller", this is purely a convention, borrowed from MVC-style frameworks, that keeps files organized as the number of routes grows.
Name controller files after the resource they handle (users.controller.js).
Export one function per route action (list, getById, create, update, remove).
Controllers can call services (see the Services lesson) instead of talking to the database directly.
Keep controllers focused on translating HTTP concerns (req/res) into calls on your business logic.
Controller Pattern Cheatsheet
How responsibilities split between router, controller, and (optionally) service layers.
Layer
Responsibility
Example
Router
Maps HTTP method + path to a controller function
router.get('/:id', ctrl.getUser)
Controller
Reads req, calls business logic, shapes response
exports.getUser = (req, res) => {...}
Service (optional)
Pure business logic, no req/res
userService.findById(id)
A Full Controller Example
A well-structured controller function validates minimal HTTP concerns, delegates the actual work, and translates the result (or error) back into an HTTP response.
Controllers that only orchestrate, rather than implement, business logic are far easier to test, reuse (e.g. from a CLI script or background job), and reason about. Heavy business logic belongs one layer deeper, in services.
Common Mistakes
Writing raw database queries directly inside controller functions instead of delegating to a service layer.
Letting controllers grow into 200-line functions instead of splitting logic into smaller helpers.
Forgetting to forward errors with next(err) inside async controller functions.
Naming controller functions inconsistently across files, making the codebase harder to navigate.
Key Takeaways
Controllers are a convention, not an Express feature, for keeping route files thin.
Each controller function focuses on translating HTTP request/response into business logic calls.
Heavy business logic should live in a service layer, not directly inside controllers.
Pro Tip
If a controller function is hard to unit test without spinning up a real Express server, that's usually a sign business logic has leaked into the controller and should move to a service.
You now know how to separate route logic into controllers. Next, take it one step further with a dedicated Services layer for business logic.