How to Structure Express Routes and Controllers
Clean structure prevents spaghetti code. Here is how to structure routes and controllers.
How to Structure Express Routes and Controllers
Clean structure prevents spaghetti code as your app grows. Here is how to structure routes and controllers.
The Layout
src/
routes/
auth.js
users.js
posts.js
controllers/
authController.js
userController.js
postController.js
middlewares/
auth.js
validate.js
error.js
models/
user.js
post.js
services/
userService.js
postService.js
validations/
auth.js
user.js
post.js
utils/
ApiError.js
asyncHandler.js
logger.js
app.js
server.js
Routes: Thin and Declarative
// routes/posts.js
const express = require('express');
const router = express.Router();
const auth = require('../middlewares/auth');
const validate = require('../middlewares/validate');
const { createPostSchema, updatePostSchema } = require('../validations/post');
const controller = require('../controllers/postController');
router.use(auth);
router.get('/', controller.listPosts);
router.post('/', validate(createPostSchema), controller.createPost);
router.get('/:id', controller.getPost);
router.patch('/:id', validate(updatePostSchema), controller.updatePost);
router.delete('/:id', controller.deletePost);
module.exports = router;
Routes declare endpoints and bind them to controllers. No logic.
Controllers: Parse, Call, Respond
// controllers/postController.js
exports.createPost = asyncHandler(async (req, res) => {
const post = await postService.createPost(req.user.userId, req.body);
res.status(201).json(post);
});
Controllers parse the request, call a service, send the response. Thin.
Services: Business Logic
// services/postService.js
async function createPost(authorId, data) {
return Post.create({ ...data, authorId });
}
async function updatePost(postId, userId, data) {
const post = await Post.findById(postId);
if (!post) throw new ApiError(404, 'Post not found');
if (post.authorId.toString() !== userId) throw new ApiError(403, 'Not authorized');
Object.assign(post, data);
await post.save();
return post;
}
module.exports = { createPost, updatePost };
Services do the work. Reusable. Testable.
Why a Services Layer
Without services, controllers grow long. Logic is duplicated across controllers. Hard to test. With services, controllers stay thin and services are reusable. You can also use services in cron jobs, webhooks, or other services without an HTTP context.
When to Skip Services
For simple CRUD, controllers can call models directly. Add services when logic is non-trivial (rules, permissions, orchestration).
The Takeaway
Structure Express routes and controllers with thin routes (declare endpoints), thin controllers (parse, call, respond), and services for business logic. Use middlewares for auth, validation, and errors. One file per concern. Add services when logic is non-trivial; skip for simple CRUD.
Thin routes that declare endpoints and bind to controllers. Thin controllers that parse the request, call a service, send the response. Services for business logic. Middlewares for auth, validation, errors. One file per concern.
Without services, controllers grow long and logic is duplicated. With services, controllers stay thin and services are reusable. You can also use services in cron jobs, webhooks, or other services without an HTTP context. Testable too.
For simple CRUD, controllers can call models directly. Add services when logic is non-trivial (rules, permissions, orchestration). Do not over-engineer simple CRUD; do not under-engineer complex logic.
Only endpoint declarations and bindings to controllers. router.get('/', controller.listPosts). No logic. No validation. No business rules. Routes are the map; controllers are the handlers; services do the work.
Parse the request (req.body, req.params, req.user), call a service or model, send the response with the right status code, and handle errors with ApiError. Check resource ownership for user-specific resources. Use asyncHandler so rejections go to the error handler.
Ready to master Node.js completely?
Want to upskill yourself, crack your next interview, and get your dream job? Join our comprehensive course to dive deeper with high-quality video tutorials, solve interview questions, and a premium community.
Master Node.js
Want to upskill yourself, crack your next interview, and get your dream job? Join our comprehensive course.

