1. A bit of architecture

OK, so we generated our new fresh project using Angular CLI but what now? Should we just keep generating our services and components into some random folders. How do we structure our project?

A good guideline to follow is to split our application into at least three different modules — Core, Shared and Feature (we will probably need more than one feature module though😉).

CoreModule

All services which have to have one and only one instance per application (singleton services) should be implemented here. Typical example can be authentication service or user service. Let’s look at an example of CoreModule implementation.

Example of CoreModule implementation with single import guard

SharedModule

All the “dumb” components and pipes should be implemented here. These components don’t import and inject services from core or other features in their constructors. They should receive all data though attributes in the template of the component using them. This all sums up to the fact that SharedModule doesn’t have any dependency to the rest of our application.

It is also the perfect place to import and re-export Angular Material components.

Example of code organization in SharedModule

How to prepare project structure using Angular CLI

We can generate Core and Shared modules right after the creation of our new project. That way, we will be prepared for generation of additional components and services right from the start.

Run ng generate module core . Then create index.ts file in the core folder and re-export the CoreModule itself. We will be re-exporting additional public services which should be available in the whole application during further development.

That being done, we can do the same for shared module.

FeatureModule

We are going to create multiple feature modules for every independent feature of our application. Feature modules should only import services from CoreModule . If feature module A needs to import service from feature module B consider moving that service into core.

In some cases there is a need for services which are shared only by some features and it wouldn’t make much sense to move them into core. In that case we can create special shared feature modules as described later in this post.

Rule of thumb is to try to create features which don’t depend on any other features just on services provided by CoreModule and components provided by SharedModule.

This will keep our code clean, easy to maintain and extend with new features. It also reduces effort needed for refactoring. If followed properly, we will be confident that changes to one feature can’t affect or break the rest of our application.

LazyLoading

We should lazy load our feature modules whenever possible. Theoretically only one feature module should be loaded synchronously during the app startup to show initial content. Every other feature module should be loaded lazily after user triggered navigation.

2. Aliases for app and environments

Aliasing our app and environments folders will enable us to implement clean imports which will be consistent throughout our application.

Consider hypothetical, but usual situation. We are working on a component which is located three folders deep in a feature A and we want to import service from the core which is located two folders deep. This would lead to import statement looking something like import { SomeService } from '../../../core/subpackage1/subpackage2/some.service' .

Definitely not the cleanest import ever…

And what is even worse, any time we want to change location of any of those two files our import statement will break. Compare that to much shorter import { SomeService } from "@app/core" . Looks better, doesn’t it?

To be able to use aliases we have to add baseUrl and paths properties to our tsconfig.json file like this…

Add following props to our tsconfig.json

We are also adding @env alias to be able to easily access environment variables from anywhere in our application using same import { environment } from "@env/environment" statement. It will work for all specified environments because it will automatically resolve correct environment file based on --env flag passed to ng build command.

With our paths in place we can now import environment and services like this…

Theoretical implementation of ExampleComponent, notice organization of imports into 3 sections

You may have noticed that we’re importing entities (like SomeSingletonService in the example above) directly from the @app/core instead of @app/core/some-package/some-singleton.service . This is possible thanks to re-exporting every public entity in main index.ts file. We create one index.ts file per package (folder) and they look something like this…

Example of re-exporting of every public entity of CoreModule with index.ts file located in core folder

In most apps components and services of particular feature module will usually only have to have access to services from the CoreModule and components from SharedModule . Sometimes this might not be enough to solve particular business case and we will also need some kind of “shared feature module” which is providing functionality for a limited subset of other feature modules.

In this case we will end up with something like import { SomeService } from '@app/shared-feature'; So similarly to core, shared-feature is also accessed using @app alias.

Module dependencies follow tree structure which looks very similar to the well-known component tree

3. Using Sass

Sass is a styles preprocessor which brings support for fancy things like variables (even though css will get variables soon too), functions, mixins… You name it…

Sass is also required to effectively use official Angular Material Components library with it’s extensive theming capabilities. It is safe to assume that using Sass it’s the default choice for most projects.

To use Sass we have to generate our project using Angular CLI ng new command with --style scss flag. This sets up most of the necessary configuration. One thing which is not added by default is stylePreprocessorOptions with includePaths and we can set it up ourselves with mandatory root "./" and optional "./themes" values.

Additional Sass configuration for Angular CLI

This helps our editor to find imported symbols and enhances developer experience with code completion of Angular Material variables and utility functions.

When theming Angular Material apps it’s a good practice to extract theme definitions into separate themes folder, one theme per file.

Follow me on Twitter to get notified about the newest blog posts and interesting frontend stuff

4. The “PROD” build

Project generated by Angular CLI comes only with a very simple ng build script out of the box. To generate production grade artifacts we have to do a bit of customization ourselves.

We add "build:prod": "ng build --target production --build-optimizer --vendor-chunk" to our package.json scripts.

Target Production

This one is an umbrella flag which enables code minification and lot of useful build flags by default. It is equivalent to using following…

--environment prod —use environment.prod.ts file for environment variables

—use file for environment variables --aot — enable Ahead-of-Time compilation. This will become a default setting in future versions of Angular CLI but for now we have to enable this manually

— enable Ahead-of-Time compilation. This will become a default setting in future versions of Angular CLI but for now we have to enable this manually --output-hashing all — hash contents of the generated files and append hash to the file name to facilitate browser cache busting (any change to file content will result in different hash and hence browser is forced to load a new version of the file)

— hash contents of the generated files and append hash to the file name to facilitate browser cache busting (any change to file content will result in different hash and hence browser is forced to load a new version of the file) --extract-css true — extract all the css into separate style-sheet file

— extract all the css into separate style-sheet file --sourcemaps false — disable generation of source maps

— disable generation of source maps --named-chunks false — disable using human readable names for chunk and use numbers instead

Other useful flags

--build-optimizer — new feature which results in smaller bundles but much longer build times so use with caution! (also should be enabled by default in the future)

— new feature which results in smaller bundles but much longer build times so use with caution! (also should be enabled by default in the future) --vendor-chunk — extract all vendor (library) code into separate chunk

Also check official docs for other available configuration flags which might be useful in your individual project.

5. Phantom JS is dead! Long live Headless Chrome!

PhantomJS is a very well known headless browser which was defacto THE SOLUTION for running frontend tests on CI servers and many dev machines.

While being kinda OK, it’s support for modern ECMAScript features was lagging. More so, it’s non-standard behavior caused headache on many occasions when tests were passing locally without problem but they still broke the CI environment.

Luckily, we don’t have to deal with it anymore!