DEV Community

Victor Tihomirov
Victor Tihomirov

Posted on • Updated on

A simple Angular folder structure that makes development feel natural and easy.

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.`);
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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!

If you liked my article and would like to buy me a coffee, you can do it here.

Top comments (4)

alaindet profile image
Alain D'Ettorre

These recommendations are generally wrong, I'm sorry.

First of all, anything regarding modules is going away in Angular eventually and I suggest to use SCAMs anyway (Single-Component Angular Modules), so that you trade a little more importing with granularity, better tree-shaking and a much easier code to adapt to standalone components.

Also, grouping things by type (types, components, etc.) is not scalable as compared to grouping by feature. I usually group by feature then I group by type internally, in the same feature. It a feature is complex and involves subfeatures, I then group by feature, by subfeature and lastly by type.

You could say the best scaffolding still consists of grouping related stuff together on a business logic basis instead of grouping by common technology (ex.: grouping enums together, components together etc)

vixero profile image
Victor Tihomirov

Hi Alain. I wouldn't necessarily call them wrong, as this is just a preference. In this article, I am providing a starting point on for the structure a project. This is not mutually exclusive with the SCAM approach or standalone components (although I didn't mention that at all). You could still make all your components standalone and split them into features. The core idea stands the same: each feature has no knowledge of the other features and one should be able to remove a feature without breaking the code.

I think you are misunderstanding the types part, maybe I should have gone into more detail. I am not suggesting to group by type, but instead extract the types into their own folder(s) and try to maintain the same folder structure as the features folder. After all, in my opinion, features shouldn't have dependencies on each other. Also, from my experience, when splitting types into global and features types, it tends to get confusing to which context does the type belong to. Also, when you suddenly have to use a type from feature A in feature B, what do you do? Do you create the exact same type in feature B or do you move it to the global types. This creates an unnecessary overhead and refactoring. What I have found out is that by making all types global and maintaining the same folder structure as the features, it avoids the overhead, refactoring and makes decisions easier.

spock123 profile image
Lars Rye Jeppesen

Great article, thank you

vixero profile image
Victor Tihomirov

I'm glad you enjoyed it.