Organize Angular v20+ Like a Pro: Scalable Folder Structure with ESLint Tips

Koustubh MishraKoustubh Mishra
12 min read

Building a scalable and maintainable Angular application starts with a well-organized folder structure and robust linting rules to enforce best practices. With Angular v20+ and the shift to Standalone Components by default (introduced in Angular 17), organizing your app effectively while ensuring architectural integrity is crucial.

This guide walks you through the best practices for structuring your Angular v20+ application, aligned with the latest Angular style guide, and includes ESLint rules to enforce key architectural principles such as preventing feature-to-feature imports, ensuring the core module is independent and eagerly loaded, and configuring features for lazy loading. Whether you're a beginner or an experienced developer, this easy-to-follow guide will help you create a clean, efficient, and well-governed Angular project.


Why Folder Structure and ESLint Rules Matter

A well-organized folder structure makes your codebase easier to navigate, maintain, and scale as your application grows. However, without proper enforcement, developers might inadvertently introduce dependencies that break modularity or violate best practices. By integrating ESLint rules, you can ensure that your Angular v20+ application adheres to architectural principles, such as:

  • Isolation of Features: Features should not import other features to maintain modularity.

  • Independent Core: The core module should be self-contained and eagerly loaded to provide essential functionality.

  • Lazy-Loaded Features: Business features should be lazy-loaded to optimize performance.

  • Consistent Code Quality: Enforce naming conventions and file organization per the Angular v20 style guide.

This guide is based on the official Angular v20 style guide and focuses on organizing the src/app folder, with added ESLint rules to enforce best practices.


Key Changes in Angular v20+

Before diving into the folder structure and ESLint rules, let’s highlight the key changes introduced in Angular v20+ and how they impact organization:

  • Standalone Components by Default: Starting with Angular 17, the ng new command generates a Standalone application, using Standalone Components, Directives, and Pipes, with no NgModules.

  • Updated Naming Conventions:

    • Components, Directives, and Services no longer use suffixes (e.g., LoginComponent is now Login, auth.service.ts is now auth.ts).

    • Pipes, Guards, Resolvers, Interceptors, and Modules retain suffixes but use a hyphen instead of a dot (e.g., auth-guard.ts instead of auth.guard.ts).

  • Focus on Scalability: The new style guide emphasizes modular, domain-driven structures for better maintainability.

This guide focuses on organizing the src/app folder, excluding default files like app.ts, app.spec.ts, app.html, app.css, app.routes.ts, and app.config.ts generated by the Angular CLI.


Types of Code in Your Application

Your Angular app’s src/app folder typically contains three types of code:

  1. Non-Business Features: Features like authentication, layout, or global notifications that aren’t tied to your app’s business domain.

  2. Business Features: Features specific to your app’s domain, such as product listings or checkout processes in an e-commerce app.

  3. Shared Code: Code reused across features, like utility functions or reusable UI components.

These types translate into a top-level folder structure:

src
└── app
    ├── core
    ├── features
    └── shared

Let’s break down each folder, how to organize it effectively, and the ESLint rules to enforce best practices.


1. Non-Business Features (core Folder)

The core folder is for features that aren’t specific to your app’s business domain but are essential for its functionality. Examples include:

  • Application layout (headers, footers, sidebars)

  • Authentication (login, registration, password recovery)

  • Global notifications

Example: Authentication Feature

Let’s use authentication as an example to illustrate how to organize a non-business feature. Common elements of an authentication feature include:

  • Pages: Login, registration, and password recovery.

  • Shared Code: User model, authentication service, and route guards.

Here’s how you can structure the auth folder:

src
└── app
    └── core
        └── auth
            ├── auth.routes.ts
            ├── pages
            │   ├── login
            │   ├── register
            │   ├── password-recovery
            ├── models
            │   └── auth.model.ts
            ├── services
            │   ├── auth-store.ts
            │   └── auth-store.spec.ts
            ├── guards
            │   ├── auth-guard.ts
            │   └── auth-guard.spec.ts

Key Points:

  • Pages Folder: Store routed components (e.g., login, register) in a pages folder to distinguish them from other components.

  • Routes: Use a dedicated auth.routes.ts file for feature-specific routes. If the feature has only one route, define it in app.routes.ts instead.

  • Shared Code: Place models, services, and guards in dedicated folders (models, services, guards) for clarity, especially if you expect multiple related files.

Inside a Page Folder

Each page folder (e.g., login) contains the routed component and any related code specific to that page:

src
└── app
    └── core
        └── auth
            └── pages
                ├── login
                │   ├── login.ts
                │   ├── login.spec.ts
                │   ├── login.html
                │   ├── login.css
                │   ├── components
                │   ├── services
                │   ├── models
                │   ├── directives
                │   ├── pipes
                ├── register
                ├── password-recovery

Isolated Non-Business Features

For simple features like a notification service or an API interceptor, you don’t need a dedicated folder. Instead, place them in technical folders (services, interceptors) at the root of core:

src
└── app
    └── core
        ├── services
        │   ├── notification-api.ts
        │   └── notification-api.spec.ts
        ├── interceptors
        │   ├── api-interceptor.ts
        │   └── api-interceptor.spec.ts
        ├── auth
        │   ├── auth.routes.ts
        │   ├── pages
        │   │   ├── login
        │   │   ├── register
        │   │   ├── password-recovery
        │   ├── models
        │   ├── services
        │   ├── guards

Note: Avoid overusing technical folders (services, interceptors) as they can make files harder to discover. Use them only for simple, isolated features.

ESLint Rules for core Folder

To ensure the core folder remains independent and eagerly loaded:

  1. Prevent Imports from features or modules:

    • The core folder should not depend on business features to remain self-contained and reusable.

    • Use the no-restricted-imports rule to block imports from features or modules.

    {
      "rules": {
        "no-restricted-imports": [
          "error",
          {
            "patterns": [
              {
                "group": ["**/features/**", "**/modules/**"],
                "message": "Core module should not import from features or modules to remain independent."
              }
            ]
          }
        ]
      }
    }
  1. Enforce Eager Loading:

    • The core module should be eagerly loaded in app.routes.ts or app.config.ts. While ESLint can’t directly enforce eager loading, you can use a custom rule to ensure core files are not referenced in lazy-loaded routes.

    • Alternatively, document that core routes should be imported directly in app.routes.ts:

    // src/app/app.routes.ts
    import { authRoutes } from './core/auth/auth.routes';

    export const routes = [
      { path: 'auth', children: authRoutes },
      // Other eagerly loaded routes
    ];
  1. Enforce Naming Conventions:

    • Ensure files in core follow Angular v20 naming conventions (e.g., auth-guard.ts, no suffixes for services like auth-store.ts).

    • Use the @angular-eslint/no-suffixes rule (custom rule, requires plugin configuration) or a custom ESLint rule:

    {
      "rules": {
        "no-restricted-syntax": [
          "error",
          {
            "selector": "ClassDeclaration[name=/Component$|Directive$|Service$/]",
            "message": "Components, Directives, and Services should not use suffixes in Angular v20+."
          }
        ]
      }
    }

2. Business Features (features or modules Folder)

Business features are specific to your app’s domain. For example, in an e-commerce app, features might include product listings, cart, or checkout processes. Instead of placing all features in a flat features folder, group them by domain in a modules folder for better scalability.

Why Not a Flat features Folder?

A flat structure like this becomes messy as your app grows:

src
└── app
    └── features
        ├── post-creation
        ├── post-list
        ├── mention-list
        ├── jobs-card
        ├── group-card
        ├── group-form
        ├── event-card
        ├── event-form
        ├── job-card
        ├── job-form

Instead, group features by domain in a modules folder:

src
└── app
    └── modules
        ├── posts
        │   ├── post-creation
        │   ├── mention-list
        │   └── post-list
        ├── groups
        │   ├── group-card
        │   └── group-form
        ├── events
        │   ├── event-card
        │   └── event-form
        ├── jobs
        │   ├── job-card
        │   └── job-form

Each domain folder (e.g., posts, groups) follows the same structure as the core folder, with:

  • A dedicated folder for each feature.

  • A pages folder for routed components.

  • Folders for shared content within the feature (models, services, guards, etc.).

Example: Checkout Feature

For a checkout feature in an e-commerce app, the structure might look like:

src
└── app
    └── modules
        ├── checkout
            ├── checkout-api.ts
            ├── checkout-api.spec.ts
            ├── checkout.model.ts
            ├── checkout-guard.ts
            ├── checkout-guard.spec.ts
            ├── checkout.routes.ts
            └── pages
                ├── address
                ├── payment

Shared Code Within a Domain

If multiple features in a domain share code (e.g., a post.model.ts used by post-creation and post-list), place it at the root of the domain folder:

src
└── app
    └── modules
        ├── posts
            ├── post-creation
            ├── mention-list
            ├── post-list
            ├── post.model.ts

Note: Unlike the core folder, the modules folder shouldn’t have technical folders (services, interceptors) at its root. All code should be organized within domain-specific folders.

ESLint Rules for modules Folder

To ensure features are modular and lazy-loaded:

  1. Prevent Feature-to-Feature Imports:

    • Features should not import other features to maintain isolation and prevent tight coupling.

    • Use the no-restricted-imports rule to block imports between features:

    {
      "rules": {
        "no-restricted-imports": [
          "error",
          {
            "patterns": [
              {
                "group": ["**/modules/**/*"],
                "importNames": ["*"],
                "message": "Features should not import other features to maintain modularity. Use shared code instead."
              }
            ]
          }
        ]
      }
    }
  1. Enforce Lazy Loading:

    • Features in the modules folder should be lazy-loaded to optimize performance. While ESLint can’t directly enforce lazy loading, you can use a custom rule to flag direct imports of feature modules in app.routes.ts.

    • Example of lazy loading in app.routes.ts:

    // src/app/app.routes.ts
    export const routes = [
      {
        path: 'checkout',
        loadChildren: () => import('./modules/checkout/checkout.routes').then(m => m.checkoutRoutes)
      }
    ];
  • To enforce this, use a custom ESLint rule to prevent direct imports of feature routes:
    {
      "rules": {
        "no-restricted-imports": [
          "error",
          {
            "patterns": [
              {
                "group": ["**/modules/**/routes"],
                "importNames": ["*"],
                "message": "Feature routes should be lazy-loaded using dynamic imports."
              }
            ]
          }
        ]
      }
    }
  1. Enforce Naming Conventions:

    • Ensure feature files follow Angular v20 naming conventions (e.g., checkout-guard.ts, no suffixes for components).

    • Reuse the no-restricted-syntax rule from the core section.


3. Shared Code (shared Folder)

The shared folder contains code reused across features, divided into two types:

  • Dumb Shared Code: Code without business logic, like UI components or utility functions.

  • Smart Shared Code: Code with business logic, like a product model or API service.

Dumb Shared Code

Dumb components, pipes, and utilities don’t rely on business logic. For example, a notification component that displays a title and message should work with any data, regardless of its source. Place these in the shared folder:

src
└── app
    └── shared
        ├── components
        │   ├── notification.ts
        │   ├── notification.spec.ts
        │   ├── notification.html
        │   ├── notification.css
        ├── pipes
        │   ├── date-pipe.ts
        │   ├── date-pipe.spec.ts
        ├── utils
        │   ├── array.utils.ts
        │   ├── array.utils.spec.ts

Smart Shared Code

For code with business logic (e.g., a product-card component or product-api service), avoid placing it in the shared folder. Instead, put it in the relevant domain folder in modules to keep it discoverable and maintainable. For example:

src
└── app
    └── modules
        ├── product
            ├── product-card.ts
            ├── product-api.ts
            ├── product.model.ts

This approach prevents the shared folder from becoming a dumping ground for all shared code, making your app easier to navigate.

ESLint Rules for shared Folder

To ensure the shared folder contains only dumb code:

  1. Prevent Business Logic in shared:

    • Use a custom ESLint rule to flag files in shared that import business-specific modules or services.

    • Example:

    {
      "rules": {
        "no-restricted-imports": [
          "error",
          {
            "patterns": [
              {
                "group": ["**/modules/**"],
                "importNames": ["*"],
                "message": "Shared folder should not import business-specific code from modules. Place business logic in the relevant module."
              }
            ]
          }
        ]
      }
    }
  1. Enforce Naming Conventions:

    • Ensure shared components, pipes, and utilities follow Angular v20 naming conventions.

    • Reuse the no-restricted-syntax rule from the core section.


Complete Folder Structure Example

Here’s a summary of a complete Angular v20+ folder structure for an e-commerce app:

src
└── app
    ├── core
    │   ├── layout
    │   ├── auth
    │   │   ├── auth-store.ts
    │   │   ├── auth-store.spec.ts
    │   │   ├── auth.model.ts
    │   │   ├── auth-guard.ts
    │   │   ├── auth-guard.spec.ts
    │   │   ├── auth.routes.ts
    │   │   └── pages
    │   │       ├── login
    │   │       ├── register
    │   │       ├── password-recovery
    │   ├── services
    │   │   ├── notification-api.ts
    │   │   └── notification-api.spec.ts
    │   ├── interceptors
    │   │   ├── api-interceptor.ts
    │   │   └── api-interceptor.spec.ts
    ├── modules
    │   ├── product
    │   ├── cart
    │   ├── checkout
    │   │   ├── checkout-api.ts
    │   │   ├── checkout-api.spec.ts
    │   │   ├── checkout.model.ts
    │   │   ├── checkout-guard.ts
    │   │   ├── checkout-guard.spec.ts
    │   │   ├── checkout.routes.ts
    │   │   └── pages
    │   │       ├── address
    │   │       ├── payment
    ├── shared
    │   ├── components
    │   │   ├── notification.ts
    │   │   ├── notification.spec.ts
    │   │   ├── notification.html
    │   │   ├── notification.css
    │   ├── pipes
    │   │   ├── date-pipe.ts
    │   │   ├── date-pipe.spec.ts
    │   ├── utils
    │   │   ├── array.utils.ts
    │   │   ├── array.utils.spec.ts

Setting Up ESLint in Your Angular Project

To implement the ESLint rules above, follow these steps:

  1. Install ESLint and Angular ESLint Plugin:

     npm install eslint @angular-eslint/schematics --save-dev
    
  2. Initialize ESLint: Run the following command to set up ESLint in your Angular project:

     ng add @angular-eslint/schematics
    
  3. Configure ESLint Rules: Create or update the .eslintrc.json file in your project root with the rules provided above:

     {
       "root": true,
       "ignorePatterns": ["projects/**/*"],
       "overrides": [
         {
           "files": ["*.ts"],
           "extends": [
             "plugin:@angular-eslint/recommended",
             "plugin:@angular-eslint/template/recommended"
           ],
           "rules": {
             "no-restricted-imports": [
               "error",
               {
                 "patterns": [
                   {
                     "group": ["**/features/**", "**/modules/**"],
                     "message": "Core module should not import from features or modules to remain independent."
                   },
                   {
                     "group": ["**/modules/**/*"],
                     "importNames": ["*"],
                     "message": "Features should not import other features to maintain modularity. Use shared code instead."
                   },
                   {
                     "group": ["**/modules/**"],
                     "importNames": ["*"],
                     "message": "Shared folder should not import business-specific code from modules. Place business logic in the relevant module."
                   },
                   {
                     "group": ["**/modules/**/routes"],
                     "importNames": ["*"],
                     "message": "Feature routes should be lazy-loaded using dynamic imports."
                   }
                 ]
               }
             ],
             "no-restricted-syntax": [
               "error",
               {
                 "selector": "ClassDeclaration[name=/Component$|Directive$|Service$/]",
                 "message": "Components, Directives, and Services should not use suffixes in Angular v20+."
               }
             ]
           }
         }
       ]
     }
    
  4. Run ESLint: Check your code for violations:

     ng lint
    
  5. Automate Linting: Add a lint script to your package.json:

     "scripts": {
       "lint": "ng lint"
     }
    

    Run npm run lint to enforce the rules during development or in CI/CD pipelines.


Key Takeaways

  • Use core, modules, and shared: Organize your app into non-business features (core), domain-specific features (modules), and reusable code (shared).

  • Group by Domain: Nest business features in modules by domain (e.g., posts, groups) for scalability.

  • Keep shared Lean: Only place dumb components, pipes, and utilities in the shared folder. Business-specific shared code belongs in modules.

  • Follow Angular v20+ Naming: Drop suffixes for Components, Directives, and Services, and use hyphens for Pipes, Guards, and Interceptors.

  • Separate Pages: Use a pages folder for routed components to distinguish them from other code.

  • Enforce with ESLint:

    • Prevent feature-to-feature imports to maintain modularity.

    • Ensure core is independent and eagerly loaded.

    • Enforce lazy loading for modules.

    • Maintain naming conventions per Angular v20+.

By following these best practices and enforcing them with ESLint, you’ll create an Angular v20+ application that’s easy to maintain, scale, and collaborate on. Start organizing your codebase and setting up linting today, and watch your productivity soar!

Reference for ES Lint very nice ebook by Thomas Trajan

Reference for Angular V20 Folder Structure by Gérôme Grignon & Angular CanIUse


What’s your favorite way to structure an Angular app or enforce architectural rules? Share your thoughts in the comments below, and let’s discuss! For more Angular tips and tricks, follow me on Hashnode and LinkedIn

0
Subscribe to my newsletter

Read articles from Koustubh Mishra directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Koustubh Mishra
Koustubh Mishra

👋 Hey, I’m Koustubh (Kos) 💼 Frontend Engineer | Angular & Node enthusiast in the making 🚀 Building beautiful UIs, exploring modern dev tools 📚 Always learning: Angular internals, Signals, React, AWS ✍️ Sharing dev insights on LinkedIn, especially around Angular & GenAI 🎨 Anime sketcher | 🏍️ Rider | 🎧 Music keeps me going | 🏋️‍♂️ Gym is my reset button