Angular architecture patterns — Detailed project architecture
Welcome back to series of blog posts about architectural patterns for frontent web applications. The code examples are written in Angular 4 but the logic is applicable in plain javascript or any modern javascript framework.
If you are new with this topic, check previously published posts:
- High level project architecture
- Detailed project architecture (current chapter)
- Additional application features
Demo application with full source code is published on Github.
In the previous chapter we have defined the main project requirements and rough picture of how the project should behave. Now we need to break apart each functional block, we drew in the first chapter on Figure 1, and see what we need to do to make everyone of them alive. Besides redefining each block, we will add a couple of extra modules to make our project complete. The extended block diagram now looks like this:
We’ll focus on application key building blocks and will give an example where to put it in the code. Async services are group of modules each responsible for handling different types of communication to the external world. State management consists of the pieces related to the ngrx library and state manipulation. Application core facade is an abstract class which holds common logic of the application core API. It includes functions that every Sandbox will inherit e.g. for getting the certain piece of the application state etc. Sandbox is a service which extends application core facade and exposes streams of state and connections to the async services.
Let’s do a quick recap of what’s going on here and how the communication is channeled through the presentational modules and application core.
- Each presentational module subscribes, through it’s own sandbox, to events published by the application core
- UI module calls one of the sandbox methods which triggers a corresponding process in async services
- Async service translates the message into suitable format and sends the request to the outer world (e.g. server request)
- Asynchronous response from the server is translated by the application core into javascript object and forwarded further to the sandbox as a stream of data.
- Each presentational module subscribed to that event gets notified through its sandbox
Folder structure
Now that we have a clearer picture of the overall design, we can see how to organize all of that in the code. We can start by creating a folder structure. It will help us visualize the problem and make it easier to start the development of each module. In practice there’s no clear cut between presentational and core layer and very often we need to mix them together because of practical reasons. We will organize our code into two main groups:
- Presentational features — logical units which represent rounded, standalone and reusable pieces of code (e.g. authentication, dashboard, user management…). The goal is to take the, e.g. authentication module and simply plug it in the next project we start working on.
- Shared features — modules used through the entire application (e.g. async services, data management, utility services, configuration…). These parts represent the backbone of our app. Most of them will stay the same on the next projects and the others will be adapted with the minimum effort.
Here is how it will approximately look like in practice.
app/
├──auth/
| ├──auth.module.ts
| ├──auth.page.ts
| └──auth.sandbox.ts
|
├──dashboard/
| ├──dashboard.module.ts
| ├──dashboard.page.ts
| └──dashboard.sandbox.ts
|
├──shared/
| ├──asyncServices/
| | └──http/
| |
| ├──store/
| | ├──reducers/
| | ├──actions/
| | ├──effects/
| | └──index.ts
| |
| └──sandbox/
| └──base.sandbox.ts
|
├──app.module.ts
├──app.module-routing.ts
├──app.component.ts
└──app.sandbox.ts
As you can see we have an individual folder for each of our presentational modules and one shared folder which keeps all of the app core logic. We need to pay attention to two things here:
- Keep the features organized and grouped in reasonably named folders
- Keep the structure as flat as possible
For those who want to learn more about conventions and application structure Angular has its very detailed style guide. This is very often hard to achieve because in complex applications it is difficult to have grouped and flat files at the same time. You’ll need to find the approach which suits the best for you. Let’s analyze each block from the diagram and see where to put it in the code.
Application core module
We can call it the root module as well and it’s located in app/app.module.ts file. It describes how the application parts fit together and it’s also the entry point used for launching the application. The main tasks for the root module are:
- Imports all other modules we want to plug in the application
- Provides services we want to expose globally inside the application and instantiate only once
- Declares the application’s root component
- Bootstraps the root component that Angular creates and inserts it into the index.html host web page
Application core facade
Application core facade is represented as a sandbox. It is an abstract class which holds common logic of the application core API. We placed it in app/shared/sandbox/base.sandbox.ts file.
Each presentational module’s sandbox will extend the base sandbox class which will act as an interface and the base class they will inherit from. Here we can define which methods and properties each sandbox instance needs to have. It will represent a contract, with method implementations which can be overridden as well.
Sandbox
Sandbox is a service which extends application core facade and exposes streams of state and connections to the async services. It acts as a mediator and a facade for each presentational module with some extra logic, like serving needed piece of state from the store, providing necessary async services to the UI components, dispatching events…
As we said there’s no clear cut between presentational and core logic so it’s tricky to define where each sandbox will live. We can put it inside the app/shared/sandbox folder, grouped by feature, or place it inside the corresponding presentational module folder. We’ll go with the second option because the sandbox logic is explicitly related to the presentational module we are building it for. This way we’ll have all related logic in one place.
An example below demonstrates a sandbox for authentication module. It handles login and password recovery actions. The sandbox imports actions from the State Management layer (because it knows that app core contains it) and defines public variables which represent an observable piece of state. For example, loginLoading$ observable emits an event every time user presses login button and waits for the server response. Our component can subscribe to the event and toggle spinner to indicate to the user that something’s happening. All this magic happens in state management layer through actions and reducers and presentational components don’t need to know anything about it. Sandbox will select needed piece of state, which will be changed depending on the dispatched action, and components will consume it. With this organization our app is driven by the events and we can say it’s reactive because it reacts on observable events.
Note: Be careful with subscribing to many events because we need to unsubscribe from them as well to avoid multiple subscriptions when user comes to the same page over again. This can lead to memory leaks.
Angular provides very handy async pipe which is used in templates and it does the job automatically.
State management
We are not going to go too deep into each piece of the state management layer because it’s not the topic of this blog post. For those ones who are new to this please refer to ngrx store documentation. Despite that, there are two things we’ll concentrate on in this chapter:
- How to organize the store
- Handling async actions with ngrx/effects
We’ll treat our store as a database where each reducer is a table and it represents a slice of state we want to keep track of. The store acts like a relational database where we can use a high level selectors to merge different parts of our state. Let’s say we have a page in our app with list of products and filter bar to search the products by name and filter them by a category. On one side we have a state with a list (array) of products and on another an object which holds a value of selected filter. In order to filter the products by selected category and name we will combine two streams of state (products and filters) and display a result. Pretty much the same logic as in real database, with JOIN selectors.
We put our store inside app/shared/store/index.ts file. It will hold an interface which describes each piece of the store and represents the state from each reducer — State. This interface is just a map of keys to inner state types. Besides overall state the store contains selector functions to get each little piece of the state and child reducers have no knowledge of the overall state tree.
Here ‘s the short version of how the store looks like:
On the other hand we are using ngrx/effects. What are they? Effects relate to the term side effects. It‘s a piece of code which needs to be executed after the ngrx action has been invoked. It’s basically a function which returns an observable.
Let’s say we need to perform an async call and change a piece of state with the given response. We would need to trigger an async call somewhere, dispatch an action to indicate loading, wait for the async response to dispatch another action for storing the data and indicate success or error response. All of these actions would end up in our sandbox.
This is where the effects come in place. Effects are used for handling async calls for our actions and chaining other actions when async calls end. This way we don’t need to bother with synchronizing the actions and async calls.
To manipulate with application state we should deal with actions only.
This way we made an asynchronous action to look more synchronous which is very natural because this is the way our brain works. We will be dispatching an actions from components to communicate with app core and http requests, web socket requests etc. will be firing in the background. The example of effect implementation is shown below:
This doesn’t mean that we’ll not be dealing with async services directly. We can call asnc service any time we need to get some data which doesn’t go in the state.
Async services
Async services are a collection of modules responsible for different types of communication. Their responsibility is to prepare the data in corresponding format, establish the communication with associated communication protocol and translate the response to application friendly format. Let’s explain how to implement an http service since it’s the most common one.
The goal of the http layer, besides the ones mentioned above, is to add headers, manage the request methods, intercept requests, receive the responses, parse them and handle the various types of errors without writing it all over again through the application.
There’s one more requirement. When using the http layer it would be very nice to have rest-like interface. This is very useful because we usually got used to rest api services on the servers and another reason is that they are very self descriptive. The goal is to have a very tiny http client with rest-like methods which we can call from the sandbox. The final solution should look like the following and it’s inspired by the angular2-rest client (Angular2 HTTP client to consume RESTful services).
With two lines of code we can write an http method and at the same time read off all information we need to know about that method. We can call this method from the code like this: updateProductById(2, { title: "Book", stock: 5 });
Let’s recap what we did here.
@DefaultHeaders decorator sets default headers for all methods in the class. @PUT decorator sets the request method to PUT type with targeted api endpoint “/products/{id}”. @Path decorator sets the given id parameter in the url. @Body decorator specifies the data, of type Product, to be sent to the server. Method returns an observable so the result can be handled in caller method as well. As you can see we did a lot with just two lines of code. There’s a bunch of additional cool stuff we can add to the method if we want to.
Let’s take a look at another example. We want to send form-data type to the server, instead of json. We also want to override the default headers and apply a custom adapter to response in order to transform the data to format suitable for our custom method. Here’s how we achieve it with our rest service.
Our http service is responsible for all this cool syntax. There’s logic behind the scene which we can’t show here entirely but in general we have a function which reads all these decorators and based on that builds a request to send to the server. It also creates hooks to intercept and modify request data or headers, catch the errors and manipulate the response data.
The final thing we need to mention are the adapters. Adapters are functions used to transform the data received from the server to format friendly for displaying by the UI components. We have base http adapter which parses the http json response object into javascript object literal. It also checks if the custom adapter has been applied by the @Adapter decorator and calls the adapter function. Custom adapter functions can be located in presentational modules if they are related to presentational layer.
Conclusion
Let’s recap what we have covered in this chapter:
- Defined more detailed project architecture and decided which part of the code goes in each application layer
- Created a basic folder structure
- Explained how application core facade and sandbox works
- Explained state management and effects logic
- Demonstrated async service implementation
Now we have a better understanding of how to decouple an application based on theory from the first chapter. It’s always harder to realize the ideas in practice and the important thing is that the practical implementation is not the identical copy of the project from the block diagram. Sometimes it’s hard to decide where to put a certain piece of the code and draw a clear line between the layers.
In the next and final chapter we’ll be talking about additional features the project should consists of and we’ll provide a full example of the project with all features included.
Originally published at netmedia.io.