- Introduction
- Controllers: Keep Them Focused
- My Preferred Controller Methods
- Actions Over Services
- Real Example: Blog System Routing
- Conclusion
Introduction
In my previous blog post, I shared my approach to structuring REST APIs effectively, focusing on clarity, convention, and scalability. That post emphasized how to keep API surface areas predictable and maintainable. Now, I want to take you a layer deeper into what happens behind the API.
In this post, I will break down how I structure the backend logic that powers my APIs, drawing from 14+ years of experience building backend applications in Node.js and PHP. The goal? Keep things simple, explicit, and future-proof.
Controllers: Keep Them Focused
Taylor Otwell, the creator of Laravel, has a rule I fully embrace: controllers should stick to the 7 standard RESTful methods. If you need to handle more complex or domain-specific behavior, you do not expand the controller. You create a new one.
One of my Laravel ✨ best practices ✨ is only using the 7 "restful" methods in my controllers.
— Taylor Otwell (@taylorotwell) April 27, 2023
Controllers only have: index, show, create, store, edit, update, delete.
Think you need another method? You really need another controller. Built *all* of Vapor like this.
This philosophy aligns perfectly with a principle I have come to live by in object-oriented environments:
You can’t have too many classes.
It might feel like overkill at first, but clear separation of concerns pays dividends as your codebase grows. By using smaller, well-named classes, you make the system easier to reason about, test, and evolve.
My Preferred Controller Methods
When I am not using a framework like NestJS or Laravel, which often scaffold controllers for you, I still stick to a minimal set of controller methods. Here is what I use by default:
- create
- read
- readAll
- update
- delete
These method names map closely to the intentions of a typical RESTful resource.
Actions Over Services
One pattern I have moved toward is using action classes instead of large service classes. Services tend to grow unchecked, morphing into god-objects that do way too much. Actions, on the other hand, are lean, purpose-driven, and easy to test.
Each controller method delegates to a single action class that handles the actual business logic. This makes responsibilities explicit and avoids bloated controllers or service layers.
Real Example: Blog System Routing
Let us revisit the blog system I used in my last post. Here’s how I would structure the backend logic using my approach:
HTTP Route | Controller | Method | Action Class |
---|---|---|---|
POST /posts | PostsController | create | CreatePostAction |
GET /posts | PostsController | readAll | ReadPostsAction |
GET /posts/:id | PostsController | read | ReadPostAction |
PATCH /posts/:id | PostsController | update | UpdatePostAction |
PUT /posts/:id/title | PostTitleController | update | UpdatePostTitleAction |
DELETE /posts/:id | PostsController | delete | DeletePostAction |
POST /published-posts | PublishedPostsController | create | PublishPostAction |
DELETE /published-posts/:id | PublishedPostsController | delete | UnpublishPostAction |
POST /posts/:id/comments | PostCommentsController | create | CreatePostCommentAction |
GET /posts/:id/comments | PostCommentsController | readAll | ReadPostCommentsAction |
GET /comments/:id | CommentsController | read | ReadCommentAction |
PATCH /comments/:id | CommentsController | update | UpdateCommentAction |
DELETE /comments/:id | CommentsController | delete | DeleteCommentAction |
Notice how:
- Each action is named after exactly what it does.
- Controllers remain lean and follow a consistent pattern.
- Even sub-resources like
comments
of a post, thetitle
of a post, and operations likepublish
/unpublish
are given their own controllers.
This structure makes it easy to trace behavior, enforce boundaries, and introduce changes without fear of breaking unrelated parts of the app.
If you would like to see the blog system example in action, I’ve published the code on GitHub. Check out the repository here: https://github.com/rkulik/nodejs-backend-skeleton
Conclusion
Backends grow. It is inevitable. But complexity does not have to be messy. By leaning into object-oriented principles, limiting controller scope, and favoring single-purpose action classes, you can build backends that are as robust and scalable as the APIs they serve.
If you liked this post, check out my previous article on designing REST APIs.