• April 28, 2024

How to develop an Enterprise-level web application

Plan
Introductory
Branching the repository
Dependency Injection
Project Setup
Middleware:
Authentication
RBAC
Entity Ownership
HTTPS
Testing
Results
Introductory
Hello everyone, today we are continuing a series of articles on how to develop a commercial web application from scratch. The code is presented in the repository.

Alexey Solomonov
Alexey Solomonov
ONE HUNDRED subscriptions “Fire”
In the previous article, we talked about how:

Collect requirements.
Develop the architecture.
Choose a web framework.
Document the API.
Obviously, the developed project is still very far from being able to work with users:

In the lifecycle of project applications, there are cases when some parameters need to be changed.
Depending on the state, different API methods are available to the user
The user can receive or change only the data that he owns.
But before we solve the described problems, it is necessary to understand:

How to support multiple versions of a project.
How to easily satisfy the dependencies of some services on others.
Branching the repository

The image shows the real life cycle of the project:

The master runs version v1.0 on production servers.
In the v1.0 version, bugs were found and hotfixes were prepared.
The hotfixes were poured into the master and uploaded to the production servers, receiving the v1.1 version of the project.
Simultaneously with the master, a new version of the project lives in the develop on the develop servers.
It is also necessary to bring hotfixes from the master to it.
In addition, developers are pouring new tasks from the features branches into develop.
Then, at some point, a slice of develop is made into the release branch.
The release branch stabilizes, undergoes regression testing and is laid out on the production servers.
This lifecycle fits perfectly into the GitFlow branching strategy.

I use this strategy in my project we:

Putting things in order in the naming of repository branches:
a. feature/task_identifier in the tracker.
b. hotfix/task_identifier in the tracker.
c. release/H.H.H.
We tie CI/CD assemblies to standardized branch prefixes.
We make independent builds of features, fixes and releases.
We get a situation in which the support of the current version does not interfere with the development of a new version of the project.
Project Team ogon.ru does not use the git-flow console program, because it does not fit into the same process with CI/CD systems like GitLab, so let’s create the necessary branches ourselves:

git checkout -b release/1.0.0 master # fixed the version of the first article
git checkout -b develop master
git checkout -b feature/article-2 develop # we will develop the second article in a separate branch

Thus we got the result:

Readers of the first article receive a working example in the release/1.0.0 branch.
The functionality of the second article will be developed in the feature/article-2 branch.
Accordingly, no one bothers anyone.
Dependency Injection
As mentioned in the previous article:

The architecture is divided into domains
The domain logic is implemented in the services.
Services depend on other services.
Let’s look at cmd/user-server/main.go:

This code is bad because we manually satisfy constructor dependencies:

usersrvimpl.NewUserProductService
userapp.NewServer
Perhaps now the problem does not seem so serious, but in a real project, one service may have a dozen dependencies, which, in turn, may have their own dependencies. As a result, the real project has a large dependency graph, which is difficult to satisfy manually.

To solve the problem, there are popular packages that satisfy dependencies:

uber fx + uber dig.
google wire.
Both options solve dependency problems, but we settled on fx + dig. fx has reflection under the hood, and at the compilation stage we will not see dependency dissatisfaction errors. However, we will see these errors when the program starts, then fx offers an application model and allows you to group dependencies. Let’s create a universal application constructor pkg/common/adapter/application/application.go:

Thus, the code has been greatly simplified, so project support will be cheaper.

Project Setup
After we have learned how to satisfy dependencies, we need to learn how to configure the project.

The project configuration package had the following requirements:

YAML support, as the most human-readable format.
Support for environment variables in the docker-compose style.
The ability to configure each package separately.
Another uber package comes to our aid, namely config.

In addition to the requirements described above, the package can:

Combine multiple configuration files.
Set default variable values.
Let’s write a small settings provider pkg/common/adapter/application/provider.go:

Application Designer:

Accepts constructors at the input.
Satisfies the dependencies of the services among themselves.
Starts as a demon or a worker.
Now let’s look at the file with the constructors pkg/user/infra/constructors.go:

var Constructors = fx.Provide(
usersrvimpl.NewFxUserService,
usersrvimpl.NewFxUserProductService,
)

As we can see, the file with constructors has a simple form. All constructors for fx have the prefix NewFx. All project packages must contain similar files with constructors.

Let’s rewrite cmd/user-server/main.go with fx in mind:

How to understand that the code is working? Of course, you can embed the code into the project and check how the application works, but this is too expensive, especially in large projects. To check the implementation of a particular interface, you need to test it. For testing, we write unit tests using the testify package.

Let’s write a test suite for the settings provider pkg/common/adapter/application/provider_test.go:

Or run the test in the terminal:

It is important to measure the code coverage with tests, because tests should check all occurrences of conditions and cycles. If you do not test the code, then the introduction of a new big feature or a major refactoring will lead to the fact that half of the functionality will break, debugging of which will be very expensive. Of course, tests need to be maintained, but they also give confidence that the code is working.

Now we can integrate the supplier into the project, for this we will add a constructor to fx pkg/common/adapter/constructors.go: