After years of working on multiple Angular projects, I have had firsthand experience with the difficulties of poorly thought out designs. Projects that were supposed to be scalable were unscalable, had unreadable code, and did not adhere to the Angular coding style guide. Luckily, the coding style guide already gives us a place to start when it comes to project structure, with a shared module and a module for each feature.
In this guide, I will go over the feature-based approach in greater detail and explain the whys and hows.
First of all, why is a well-designed folder structure important?
- It allows us to be more flexible with our application. We can add new functionality without breaking anything.
- A well-designed system is simple to understand and navigate.
- It improves testing, maintainability, and reusability.
I like to split the project into six main folders, each with its own responsibility: core
, features
, shared
, apis
, types
, and store
.
Core folder
The CoreModule
is located in the core folder and contains essential services, components, and other functionality that are required by many parts of the application, either once (components) or as a singleton (services). Here are some examples of such components and services:
- Components (header, footer, navbar, error, etc.)
- Logging service
- Exception handling service
- Configuration service
- Localization service
- Auth service
- Http interceptors
- Guards
By encapsulating this functionality into a single module, we gain a centralized location for managing dependencies and configuring the application at a high level.
The CoreModule
is typically imported once in the application's root module. Its components and services are made available to other modules via dependency injection.
To avoid accidentally importing the CoreModule
more than once, include the following code in the constructor:
constructor(@Optional() @SkipSelf() parentModule: CoreModule) {
if (parentModule) {
throw new Error(`CoreModule has already been loaded. Import it in the AppModule only.`);
}
}
This is how the core folder might look.:
|-- core
| |-- components
| | |-- header
| | | |-- header.component.ts
| | | |-- header.component.html
| | | |-- header.component.scss
| | |-- footer
| | | |-- footer.component.ts
| | | |-- footer.component.html
| | | |-- footer.component.scss
| |-- services
| | |-- auth.service.ts
| | |-- logging.service.ts
| | |-- exception.service.ts
| |-- interceptors
| | |-- auth.interceptor.ts
| |-- guards
| | |-- auth.guard.ts
| |-- core.module.ts
Shared folder
The shared folder contains the SharedModule
which typically contains reusable UI components used throughout the application, such as buttons, form fields, and input validation. It may also include directives and pipes that are used by numerous modules.
Developers can avoid code duplication across multiple modules by encapsulating these shared components in the 'SharedModule,' which would otherwise result in code bloat and maintenance issues. It also helps to enforce UI consistency because all modules will be using the same shared components.
Bonus: Create a ThirdPartyModule
to differentiate between own components and vendor components such as NgbModule
. This module should be imported into the SharedModule
.
The shared folder might look like this:
|-- shared
| |-- components
| | |-- spinner
| | | |-- spinner.component.ts
| | | |-- spinner.component.html
| | | |-- spinner.component.scss
| | |-- modal
| | | |-- modal.component.ts
| | | |-- modal.component.html
| | | |-- modal.component.scss
| |-- directives
| | |-- highlight.directive.ts
| |-- pipes
| | |-- capitalize.pipe.ts
| |-- shared.module.ts
| |-- third-party.module.ts
Features folder
This folder contains the features of the application. Each feature has its own module that contains a collection of components, services, directives, pipelines, and other code that encapsulates a specific aspect of the application's functionality.
I like to think of a feature as a slice of pie. You should be able to easily remove a slice of pie without making a mess. This means that our features should not be dependent on one another.
The features folder can look like this:
|-- features
| |-- products
| | |-- components
| | | |-- product-list
| | | | |-- product-list.component.ts
| | | | |-- product-list.component.html
| | | | |-- product-list.component.scss
| | | |-- product-details
| | | | |-- product-details.component.ts
| | | | |-- product-details.component.html
| | | | |-- product-details.component.scss
| | |-- product-root.component.html
| | |-- product-root.component.ts
| | |-- product-routing.module.ts
| | |-- product.module.ts
Apis folder
The apis folder is used to store files related to consuming RESTful APIs or other web services. These files may include service classes, models or interfaces, and helper functions related to working with the APIs.
|-- apis
| |-- product.service.ts
Types folder
The types folder is a location for storing type definitions that are used across the application.
|-- types
| |-- user
| | |-- user.ts
| | |-- user-status.enum.ts
| |-- product
| | |-- product.ts
| | |-- product-type.enum.ts
Store folder
If you're using NgRx
for state management, the store folder is ideal for providing a centralized location for managing application state, which can help to simplify the codebase and make complex state interactions easier to manage.
|-- store
| |-- user
| | |-- actions
| | | |-- user.actions.ts
| | |-- reducers
| | | |-- user.reducers.ts
| | |-- selectors
| | | |-- user.selectors.ts
| |-- product
| | |-- actions
| | | |-- product.actions.ts
| | |-- reducers
| | | |-- product.reducers.ts
| | |-- selectors
| | | |-- product.selectors.ts
| |-- store.module.ts
In conclusion, the Angular world and the best practices are evolving, and so do we. I will come back again in a year and revise what can be improved (hello, signals!).
Keep learning, stay curious, and never stop coding!
Top comments (2)
Great article, thank you
I'm glad you enjoyed it.