Beyond the API: How I Structure the Backend Logic of My Applications

René Kulik on 22.04.2025

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.

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:

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:

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.