Applying Principles of Clean Architecture to Enterprise Frontend Application

Table of Contents

Intro

Requirements Analysis

Setup

Getters

Mutations

Actions

Introducing Storage

Connecting the dots

Conclusion

Into

It is time for the third post in the series. In the first one, we talked about Entities and the essential Business rules of our Blog. We dedicated the second tutorial to uncover how these rules can be enforced and set in motion. In this post, I will focus on the global Store of the App: Vuex.

There are thousands and thousands of posts/videos/talks about Vuex and how it works. I won’t spend time covering the basics but just quickly remind the conceptual idea. Vuex, following the Flux pattern, consists of 3 main agents:

actions

mutations

store

Vuex

The last one is a storage of data within the application that provides access to it for every single UI component. Mutations are mere functions whose responsibility is to mutate the Store. And finally, actions are functions that may perform side-effected operations and commit mutations based on that. There is also a notion of “getters”: optional helper-functions that provide access to most used parts of the Store.

It is common to put business logic inside the actions/mutations. After all, they are responsible for mutating the State. But as we discussed, business logic must not be coupled with UI. And Vuex is part of the UI, no matter how hard it pretends not to be. The only responsibility of Vuex is to be a:

centralized store for all the [UI] components in an application, with rules ensuring that the [UI] state can only be mutated in a predictable fashion https://vuex.vuejs.org

The reasons why the state changes are taking place and deep business reasoning behind the rules of these changes is not Vuex’s concern.

Previously, we defined Entities and Services who possess the knowledge of that business rules and operate them. But how can we build a bridge between Vuex Store and Services/Entities?

From the Vuex perspective, Services are “side-effects.” They exist outside of the ecosystem of Vue-Vuex and have the power to affect the State. In Vuex there is a dedicated persona, who’s accountable to deal with the side-effects: Actions

Vuex + Service

A classic example of side-effect is HTTP requests. Services are no different, as far as Vuex is concerned, from making HTTP requests, communicate with Sockets, or deal with any other sort of side effects.

The difference in architecture is significant, however. If we allow Actions to make HTTP requests directly, they would have to know how to operate the data from the API. Even worse, they would have to know how to validate and prepare data before sending it to the backend. This is a coupling of UI (Store) with business logic. And we should avoid it as much as possible. To prevent this coupling, Actions will only be invoking services without any information about how the latter ones operate.

Requirements Analysis

Photo by Helloquence on Unsplash

Business requirements didn’t change since the last time we talked about them. Let’s remind ourselves of what they are:

As a User, I should be able to see all the Articles on the Home page

As a User, I should be able to navigate from there to a page that represents one particular Article and see full Article

As a User, I should be able to leave a comment on this page

We finished the technical tasks that prepare Services for us. It’s time to define tasks for the Store:

UI Components should have the opportunity to access all articles from the global Store easily.

The Store should be updated every time the User creates a new Comment.

Setup

Photo by Alexandru Acea on Unsplash

The repo is available on Github

The demo is available here

If you completed the previous tutorial, you can continue working on your current branch. But feel free to switch to the “services” branch that contains completed previous tutorials.

In the simplest scenarios, all actions, mutations, and getters located in one single file. But I’m going to define separate folders for them. The reasoning is that it will allow me to scale and to test a bit easier. Each folder will have a familiar structure: types, spec, mock, barrel file, and the actual action/mutation/getter and the Store itself:

Store:

/src/store/store.mock.ts

/src/store/store.ts

/src/store/store.types.ts

Actions:

/src/store/actions/actions.mock.ts

/src/store/actions/actions.spec.ts

/src/store/actions/actions.types.ts

/src/store/actions/actions.ts

/src/store/actions/index.ts

Getters:

/src/store/getters/getters.mock.ts

/src/store/getters/getters.spec.ts

/src/store/getters/getters.types.ts

/src/store/getters/getters.ts

/src/store/getters/index.ts

Mutations:

/src/store/mutations/mutations.mock.ts

/src/store/mutations/mutations.spec.ts

/src/store/mutations/mutations.types.ts

/src/store/mutations/mutations.ts

/src/store/mutations/index.ts

Let’s first update all barrel files and forget about them for a while:

Barrel files

Note: in real-life application, it is also wise to separate Vuex Store onto namespaces and create folders and files for every action/mutation/getter for every domain. But since our App has only one domain (Articles), it feels unnecessary complex.

Also note: I didn’t create a spec for the Store since all it does is combine together Actions, Mutations, and Getters. I didn’t find too much value in testing this case, but your application may be different.

I’ll start with the types for the Store. The most critical type we have to provide is an interface for the State. Vuex operates two terms: root state and the actual State. Also, we can create a convenience alias for the Store itself (which is simply a generic derived from the State):

Store types

As you can see, the Store will contain an array of IArticlesData, providing access to data for UI components. We are not going to store class instances in the Store (which is usually not recommended), but rather raw data and instantiate Article when data is required.

Getters

Photo by Franck V. on Unsplash

Let’s start from the simplest one: getters. It is a plain object with functions that grab data from the Store, instantiate a class based on that data, and returns it:

Getters types

Note the explicit definition of “this.” Vuex will call these functions in the Context of the Store and knowing that will allow us to take some advantage in the future.

Tests are quite simple: “getAllArticles” must return all Articles, while getOneArticlesById must return one article by id or undefined if data is absent:

Getters spec

Note that to test correctly, we have to call getters in the context of the Store. It is the way how Vuex calls them.

This test (as many others) requires “mockState” and “mockStore.” Let’s define these mocks before we go any further:

Store types

Store mock

You may wonder where additional functions, like “dispatch,” “commit,” “subscribe” come from in “mockStore.” They are part of the Vuex Store interface. Since IStoreMock extends IStore, we have to implement and mock all public methods of the Store. Plus, I find it useful to mock and spy on them in the tests.

Finally, we can now implement the Getters themselves:

Getters

and their mocks:

Getters mock

Mutations

Photo by Chris Lawton on Unsplash

The next step is Mutations, the heart of the Vuex Store. We have only two types of mutations: the first one allows us to stock all Articles in the Store, and the second one adds new Comment to existing Article.

The first mutation expects an array of Articles as a payload, while the second one requires reference to the Article and Comment data.

Let’s define interfaces:

Mutations types

Spec should test that “fetchArticles” puts data into the Store, and “createComment” creates Article if it doesn’t exist and puts Comment to the Article:

Mutations spec

It would be nice to have a mock for payload, so let’s define it:

Mutations mock

And implement the mutations:

Mutations in the flesh

Actions

Photo by Matthew Brodeur on Unsplash

Now, Actions are a little bit trickier. They communicate with the outer world; they handle side-effects. For our application, we will implement two actions: one that is responsible for fetching Articles and the other that handles the Comment creation process.

How exactly do they do that? First, Actions invoke Services and grab all the required data. Then they use this data to commit mutations and save it into the Store.

Services’ “getAll” method expects no parameters. Fetching Action expects nothing as well. On the other hand, the “createComment” method of Service requires Article’s id and Comment’s data. So, the similar payload we have to provide for the Action.

Actions types

Note: Context is just an alias for Vuex’s ActionContext applied to our State.

Also, let’s define mocks:

Actions mock

Note: Don’t forget that Vuex calls Actions with a special ActionContext parameter. We have to mock it as well.

Tests must be pretty straightforward. “fetchArticles” action should call Service and then commit mutation.

But how?

At this point, we have no connection, no “bridge” between the Service and Store. If you recall, we created a dedicated provider that gives access to the service instance. Now we have to inject it into the Store.

Vuex is part of the Vue infrastructure and plays by its rules. Specifically, it means that we can use a neat feature called plugins to adjust the Store context the way we want.

Navigate to /src/ui/plugins. You can see there is a Vuetify plugin in there, and we are going to create another one. Call it “services.ts” and add this:

Services plugin

Don’t forget to update barrel files:

Plugins barrel file

In this plugin, we define a function that injects an instance of services (provided by the Provider, pardon my tautology) and injects it into both the Store and every Vue component.

However, this code doesn’t work out of the box. You see, neither Vue nor Vuex has a clue about the existence of Services at a compile-time. Technically speaking, their types do not contain “$service” property. This is why we receive errors like “Property ‘$services’ does not exist on type ‘Vue.’”

We have to adjust Vue and Vuex types to respect this property, which we just injected into them. We will do the same way Vuetify solves this problem: by providing a custom shim file.

In /src/ui folder, you may see a few shim files. Let’s define another one, “shims-provider.d.ts,” with this content:

Provider shim

Now both Vue and Vuex types are aware of services at the compile time.

Why dollar sign prefixes property name? It is a common convention in the Vue ecosystem. You can find $vuetify, $props, $store properties etc.

Finally, let’s put the plugin to work. In the /src/main.ts instantiate the Store and call “prepareServices”:

Apply Provider plugin at boot time

We have to update the Store mock since it now holds the reference to the Service:

Updating Store mock and types

Back in Actions, we can verify that Services are now accessible within execution context:

Actions have access to Service now

The next step is committing the mutation. Since Vuex executes Action with the Context as a first parameter, we can easily invoke mutation:

Actions

And even though technically this may work, it is not a scalable and safe approach. The first thing to notice is that I am using a string as a mutation name.

It might be a problem in the long run: somebody can change the name of mutation, and suddenly, this code doesn’t work anymore. Even worse, it may stop working without any explicit errors: the mutation simply won’t be committed.

To avoid this problem, it is common practice to use constants as names of mutations/actions/getters:

Pseudo code

While this approach is entirely legit, it doesn’t solve a critical piece of the puzzle: payload’s signature. Constant names of the Actions/Mutations/Getters give no hints nor constraints about the type of payload they expect.

Take a closer look at the Actions. ‘commit’ method accepts anything as the second parameter. We, as mutations consumers, have no knowledge about data we have to provide. The only way to know is by looking up the source code of mutation. And I had an entire article, arguing why different parts of the application should remain unknown, black-boxed. And finally, providing incorrect data may cause runtime errors, to catch which we would have to go through a very long feedback loop.

We can do better than that with TypeScript.

Introducing Storage

Photo by frank mckenna on Unsplash

The idea is simple: we create an object with a clear interface that will invoke Vuex methods under-the-hood. This ‘proxy’ is a part of the Store in the dependency circle. Consumers of the Store (first of all, UI Components) would have a robust API to use: they would know for sure what actions/mutations/getters are supported and what payload they expect. I will call this module Storage.

Why, “Storage?” No particular reason. It feels a subjectively sound since it resonates with the term “Store.” But if you don’t like it, you can use any other name.

I start with creating a “storage” folder within the Store and filling it in with empty files following the familiar structure:

/src/store/storage/storage.ts

/src/store/storage/storage.types.ts

/src/store/storage/storage.mock.ts

/src/store/storage/storage.spec.ts

/src/store/storage/index.ts

and re-exporting all in the barrel files:

Barrel files

Storage itself is just an object that holds methods for every getter/mutation/action with a proper payload:

Storage types

The responsibility of each method is to call a respective API of the Store. We can quickly test that by ensuring the API method is called:

Storage spec

The actual code is relatively simple. Again, all the Storage does at the runtime is proxying the Store:

Storage

I’m using a factory function to inject a reference to the Store into the Storage object and return it.

Let’s not forget about the mock:

Storage mock

We are going to use the Storage in many UI Components and even the Store itself, meaning we should inject it the same way we did with Services.

First, let’s define a Vue plugin for it:

Storage Vue plugin

Then initialize the Storage at the boot time:

Apply Storage plugin at boot time

And finally, let’s extend types of both Vue and Vuex:

Storage shim

and update the Store mock:

Update Store mock to use Storage mock

Note: There are a few libraries out there providing strong typing solutions for Vuex. It is up to you whether to use them instead of this Storage. I personally, not in favor of adding another dependency to the code, especially since we can accomplish our goal with just a few lines of code.

Connecting the dots

Photo by israel palacio on Unsplash

Nothing stops us from benefiting from convenient Storage API. Let’s try it out in the Actions. Instead of directly committing ‘fetchArticles’ mutations, I will use Storage:

Fetch Article Action

Simple yet powerful. Consumers now don’t have to guess what actions or mutations or getters are available and what their signatures are. We will take full advantage of Storage in the next post. For now, let’s wrap up the Store.

There is one more Action we must provide, “createComment,” which invokes Service and then commits mutations:

Create Comment Action

Let’s cover Action with a test. At this point, it as simple as ensuring that both Service and Storage has been invoked with proper data:

Actions Spec

And finally (finally!), we can construct the Store itself:

The Store

At this point, the code should compile without any errors, and the test should pass with 100% coverage:

All code complies, and tests pass

Conclusion

It was indeed a long-long journey. Let’s take a second to reward ourselves for a great job that has been done.

We started with developing Getters: access point of the Store. Then we implemented Mutations responsible for the essential part of the Vuex: changing the State. Then we took a quick detour to build the Storage: flexible and powerful API for the Store. Then we used the Storage in Actions and wrapped up with factoring the Store itself by combining Getters, Mutations, and Actions under one roof.

Our next step is UI Components. We will see how Storage simplifies access to the Store and how all this finally clicks together into something we can see at the screen.