BECAUSE SUITABLE DIRECTORY STRUCTURES AREN’T ALWAYS EASY…

Building an Angular Application Structure

Nils Hoffmann
Hyand Blog

--

When building web applications from scratch there is always a simple question in mind: How can I structure my application? It’s often hard to find a suitable directory structure for a complete project from the beginning. Often there are company standards, one is inspired by other projects or just tries to figure it out by himself.

To help out with the struggle of having think too deeply about those problems, some ideas have been gathered, sorted and verified to provide a structure that is scalable, simple and intuitive.

Using Guidelines

While reading through other articles, trying out structures and verifying ideas, there was one source of thoughts always on my mind: The Angular Styleguide. If you haven’t read it, please do so! It not only provides useful advice for the written code itself, there are also general rules to follow.

04–01: LIFT: Do structure the app such that you can Locate code quickly, Identify the code at a glance, keep the Flattest structure you can, and Try to be DRY.

What the Styleguide suggests above are some ideas on how to handle structures and mindsets on how to design the application. While it’s suggested to keep the application as flat as possible, we should also be able to locate code quickly. Based on those guidelines, a structure can be created which is stable, easy to understand, simple in extension and hopefully compatible for most use cases.

General Structure

One of the possible technologies at GOD is Java, thus when talking about a UI Module we mostly talk about a Maven Module somewhere within the project structure. But in this post we only will care about Angular-related technologies (and even of those, some entries will be omitted or simplified). So let’s get started with a high level view onto the project root.

ui-module-root/
├── package.json
├── tslint.json
├── tsconfig.json
├── angular.json
├── src/
├── mocks/
└── e2e/

If you have ever created an Angular application, all but two files & directories (actually, probably just one) should be somehow familiar. The e2e directory was automatically generated up until Angular 12, which dropped the Protractor as default e2e testing tool. The mocks directory should contain files related to mocked environments and is totally optional.

The Source

Now we look at the heart of this structure, the source folder. It contains the project stub Angular CLI generates for us. By default it contains three directories, all of them being necessary — some more, some less.

src/
├── app/
├── assets/
└── environments/

Application building and structuring

When building a single application the app/ directory is the center of the code. It all starts of the the app.module where the bootstrapping happens. There is nothing special or specific about this module. When configured, it supports lazy loading of modules via routing. For the rest of this article, we assume that lazy loading and routing is used. What follows are the core and shared module, which get directly imported within the app.module.

Module Structure

Before taking the deep dive into the single modules, we take a look on the general layout of the directories.

app/
├── core/
├── modules/
│ ├── home/
│ │ └── home-submodule/
│ └── overview/
└── shared/

The first layer of directories directly contains two modules (core/ and shared/) and another directory for all the others (modules/). The next sections will describe each of the modules separately.

Core Module

While the app.module is the one that gets bootstrapped, the core.module is the actual center of the application. It is loaded directly by app.module, thus it is not loaded lazily. Generally it contains singleton services, components which are necessary for the application (but should not be shared, like a header or footer) and other centralised building blocks of the application. In this example those are guards and interceptors. With this information, the core.module (including exemplary files) could look like the following tree:

core/
├── core.module.ts
├── components/
│ ├── footer/
│ │ └── footer.component.(html, scss, ts, spec.ts)
│ ├── header/
│ │ └── header.component.(html, scss, ts, spec.ts)
│ └── main-menu/
│ └── main-menu.component.(html, scss, ts, spec.ts)
├── guards/
│ └── auth.guard.(ts, spec.ts)
├── interceptors/
│ └── http-request-logger.interceptor.(ts, spec.ts)
├── models/
│ ├── author.model.ts
│ └── books.model.ts
└── services/
├── author.service.(ts, spec.ts)
└── books.service.(ts, spec.ts)

While components, guards, interceptors and services are directly referenced (i.e declared, provided or exported) by the module itself, the model classes are simply located there. The reason behind this choice is simplicity caused by a functional point of view (generally there is no better place for central models to reside than in the core of the application). Certainly when using wrapper- or helper-classes they can be located next to components, etc. — which could be the case when using distinct table-models for each table throughout an application.

Furthermore components within the core.module should be encapsulated within an accordingly named directory. For example header and footer:

components/
├── footer/
│ ├── footer.component.html
│ ├── footer.component.scss
│ ├── footer.component.spec.ts
│ └── footer.component.ts
└── header/
├── header.component.html
├── header.component.scss
├── header.component.spec.ts
└── header.component.ts

One of the most crucial points of the core.module is its loading behaviour: We want to have it loaded exactly once to prevent any import or duplication issues. In older versions of Angular additionally problems with singleton services and their providers can be prevented here.

To achieve this behaviour a guard should be implemented within the constructor. When using the following code, the application errors if core.module is loaded more than once.

@NgModule({...})
export class CoreModule {
constructor(@Optional() @SkipSelf() parentModule: CoreModule) {
if (parentModule) {
throw new Error(`${parentModule} has already been loaded. Import Core module in the AppModule only.`);
}
}
}

Shared Module

The next module on the list is the shared.module. As the the name suggests it contains shared components, directives and pipes which are used by multiple modules and their components. As the shared.module is normally loaded alongside the app.module it is also loaded eager — like the core.module. Contrary to the core.module it can (and must) be imported into multiple modules when any of the components, directives or pipes should be reused. As already mentioned within the core.module, all elements should have an encapsulating directory.

shared/
├── shared.module.ts
├── components/
│ ├── details-card/
│ │ └── details-card.component.(html, scss, ts, spec.ts)
│ └── generic-table/
│ └── generic-table.component.(html, scss, ts, spec.ts)
├── directives/
│ ├── click-outside/
│ │ └── click-outside.directive.(ts, spec.ts)
│ └── element-same-size/
│ └── element-same-size.directive.(ts, spec.ts)
└── pipes/
└── text-filter/
└── text-filter.pipe.(ts, spec.ts)

Even more modules

To build an application only with the core and shared module seems strange, doesn’t it? Because those two sections are not enough, there is a modules directory. In nearly all cases an application consists of many modules, which may or may not have have sub-modules of the same structure. A module will always have a name and with the assumption of routing, it will also have {module-name}-routing.module. Let’s take a look at the structure first:

modules/
├── home/
│ ├── home.module.ts
│ ├── home-routing.module.ts
│ ├── components/
│ │ └── example-component/
│ │ └── example.component.(html, scss, ts, spec.ts)
│ ├── pages/
│ │ └── home-view.component.(html, scss, ts, spec.ts)
│ └── services/
│ └── example-service.service.(ts, spec.ts)
└── overview/
├── components/
├── pages/
├── services/
└── sub-modules/

It shows two modules, whereas the home.module is the one with the example data — so we focus on the home/-directory and its module. The home.module itself is target of the routing of the app.module, while the home-routing.module is just imported. As the name states, the {module-name}-routing.module configures the routes for the actual module and its pages.

What follows is the question ‘What is the difference between pages and components?’ While both are components in terms of Angular, pages do not have a selector — because they do not need one. Pages are components which get loaded when they are a target of routing, resulting in loading them to a router-outlet. Actual components need the selector as they are used in the templates of pages to be displayed.

Services within such a module are only loaded when the module itself is loaded. In this case example-service can be used to save a state for the module or distribute data between components when Inputs and Outputs get to complex. In other words: They are provided only for the home.module.

Generally every module is able to have any amount of sub-modules. In the example above, this is shown for the overview.module (but without example data). The structure of such a sub-module is the same as just described for the home.module.

Managing Assets

The next part is not about modules or components anymore, it is about assets. Structuring them can be difficult when project evolves. With a growing project, the assets grow and clutter its home: the assets/ directory.

Organising the structure of this directory to the point where everything is in a logically correct place with a still readable and accessible path is somewhat necessary. Because of this necessity we need an easy solution to create clusters of assets. There is a fairly easy idea to come up with: grouping assets by their purpose and name the clusters accordingly. The following example shows the favoured solution for this article and the underlying project.

assets/
├── styles/
│ ├── fonts/
│ ├── partials/
│ ├── themes/
│ │ ├── bootstrap-blueberry/
│ │ └── bootstrap-wheat/
│ ├── application.scss
│ └── application-additional.scss
├── img/
│ └── some-logo.png
├── i18n/
│ ├── de.json
│ └── en.json
└── favicon.ico

The first batch of files can roughly be described as ‘everything related to styling the application’. While this open for interpretation, those are in fact (with the premise of using SCSS in your project):

  • A core stylesheet
  • At least one stylesheet for overrides
  • Zero to N partial files, grouped within the partials directory
  • Zero to N fonts (when not used via CDN), grouped within the fonts directory

But what if there are themes, for example for example, which are not bundled in some npm package? Easy way to go is the themes/ directory. Grouped in subfolders with the containing a theme, it can be included within the angular.json or the appplication.scss. One additional thing to note is the favicon.ico: Normally it is located next to the index.html file, which requires an additional entry within angular.json. In the above example it is located within the assets — because it is an asset.

The next category that gets its own directory are the images. Depending on how many images you have they can either rest flat in their directory or have a folder structure by your own needs. To define a limit on when to use substructures is not easy and probably not even relevant. If the images are from multiple categories, group them. If you have five images of the same category, I’d keep them flat in their directory.

Having applications with hardcoded text in exactly one language sounds good. As long as don’t plan to publish this application somewhere for people that use more than a single language. So we need to be sure our application can be localised for our needs. The key to this section is to use some kind of internationalisation, in the current example those would be i18n files (used by the most common angular internationalisation tool ngx-translate— json files that have the same structure in every language, but differ in name and translations. By default the files have an ISO 3166 Alpha2 Code for their name. As a static path is necessary when loading a language, all the files must be kept in a flat structure within a single directory.

Environmental Differences

The most non-relevant one is probably the environments directory — at least when it comes to application architecture. Regularly it will only contain a hand full of files, flat with the naming convention applied to environment files. Because there is not that much to discuss, we will simply skip the journey through this directory.

Mocking the World

ui-module-root/
└── mocks/

The mocks folder is totally optional and probably not necessary for the most projects. The use is fairly easy: When using external tools like the JSON-Server or something similar which uses an external configuration file for data, it can be found in here. It is not located in assets because it would result in packaging unnecessary data inside the distribution. To keep everything accessible and clean, the mocks folder was created.

References

--

--