In today's world web applications are all the rage. So, when designing web application, either monolithic or microservice type, there are some ground rules that should be followed to make your app manageable down the line.


Let's start by defining architecture. It is wise to modularize your app so that it consists of modules that are responsible for discrete app functionality.
By discrete I mean one module is doing specific business task, like authentication, customer signup, invoicing, stock management, etc..
This modules can and should be organized in hierarchical order, something resembling a tree.

Module can do one task and not be linked to other modules, i.e. authentication and JWT issuing. This module could connect to database, look for user and if user exist return JWT for that user. End of story.

On the other hand module can call other modules, i.e. purchase module might call invoicing module to create invoice for purchased goods and purchase module can  call stock keeping module to update quantity of products in stock after purchase.

In many cases developers try making something like described structure but get entangled along the way which results with hard to follow code and literal landmine field as future developers / code maintainers or even original code author forgets after a while why and how specific things are developed. Changing seemingly benign parts of this code can cause cascading errors down the line.

Sometimes developers try to squeeze multiple functionalities through one API call.
For example, creating new customer in system might create new subscription for that customer and do some other tasks. So the customer module gets called to create new customer. But, oh boy.. customer service then goes and creates new subscription for that customer and saves it in the database. All that in customer module! Well, what's the trouble? It's just creating some simple subscription, no harm done. But, we have subscription module to do that task.
Some other developer has already created "CreateSubscription" method that should be called.
This creates spaghetti code and hard to maintain software. Since there is subscription module in the system, one might think: "To fix the bug  I need to go to subscription module and fix the issue". That is not the case with described design, one first needs to figure out that issue is in customer module and fix bug there. Probably also do some redesigning.

The correct procedure would be that customer module creates customer and after it is done call subscription module to create new subscription for new customer.
We have separated our concerns!
So now, if there is an issue related to subscription, it is natural to go to subscription service, fix the issue there and be sure that it is the only place where subscription related functionality is held.

Can you recommend any frameworks that help achieve proper separation of concerns?

What we at Continium Labs often use are frameworks that rely on Dependency Injection.

If you are on nodeJS, I would recommend you to use NestJS and Typescript.
Nest coupled with Typescript supports dependency injection which means any module can inject any other module and thus achieve proper separation of concerns.

If .net is your cup of tea then go with ASP .net Core.
ASP .net Core is built around dependency injection, is strongly typed and is multi-platform which means your code can run on Windows, Linux, Mac. This makes your hosting options versatile.