Organize Angular v20+ Like a Pro: Scalable Folder Structure with ESLint Tips
Build Maintainable Apps with Best Practices and Rules

π 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
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
coremodule 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 newcommand 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.,
LoginComponentis nowLogin,auth.service.tsis nowauth.ts).Pipes, Guards, Resolvers, Interceptors, and Modules retain suffixes but use a hyphen instead of a dot (e.g.,
auth-guard.tsinstead ofauth.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:
Non-Business Features: Features like authentication, layout, or global notifications that arenβt tied to your appβs business domain.
Business Features: Features specific to your appβs domain, such as product listings or checkout processes in an e-commerce app.
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 apagesfolder to distinguish them from other components.Routes: Use a dedicated
auth.routes.tsfile for feature-specific routes. If the feature has only one route, define it inapp.routes.tsinstead.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:
Prevent Imports from
featuresormodules:The
corefolder should not depend on business features to remain self-contained and reusable.Use the
no-restricted-importsrule to block imports fromfeaturesormodules.
{
"rules": {
"no-restricted-imports": [
"error",
{
"patterns": [
{
"group": ["**/features/**", "**/modules/**"],
"message": "Core module should not import from features or modules to remain independent."
}
]
}
]
}
}
Enforce Eager Loading:
The
coremodule should be eagerly loaded inapp.routes.tsorapp.config.ts. While ESLint canβt directly enforce eager loading, you can use a custom rule to ensurecorefiles are not referenced in lazy-loaded routes.Alternatively, document that
coreroutes should be imported directly inapp.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
];
Enforce Naming Conventions:
Ensure files in
corefollow Angular v20 naming conventions (e.g.,auth-guard.ts, no suffixes for services likeauth-store.ts).Use the
@angular-eslint/no-suffixesrule (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
pagesfolder 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:
Prevent Feature-to-Feature Imports:
Features should not import other features to maintain isolation and prevent tight coupling.
Use the
no-restricted-importsrule 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."
}
]
}
]
}
}
Enforce Lazy Loading:
Features in the
modulesfolder 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 inapp.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."
}
]
}
]
}
}
Enforce Naming Conventions:
Ensure feature files follow Angular v20 naming conventions (e.g.,
checkout-guard.ts, no suffixes for components).Reuse the
no-restricted-syntaxrule from thecoresection.
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:
Prevent Business Logic in
shared:Use a custom ESLint rule to flag files in
sharedthat 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."
}
]
}
]
}
}
Enforce Naming Conventions:
Ensure shared components, pipes, and utilities follow Angular v20 naming conventions.
Reuse the
no-restricted-syntaxrule from thecoresection.
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:
Install ESLint and Angular ESLint Plugin:
npm install eslint @angular-eslint/schematics --save-devInitialize ESLint: Run the following command to set up ESLint in your Angular project:
ng add @angular-eslint/schematicsConfigure ESLint Rules: Create or update the
.eslintrc.jsonfile 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+." } ] } } ] }Run ESLint: Check your code for violations:
ng lintAutomate Linting: Add a lint script to your
package.json:"scripts": { "lint": "ng lint" }Run
npm run lintto enforce the rules during development or in CI/CD pipelines.
Key Takeaways
Use
core,modules, andshared: Organize your app into non-business features (core), domain-specific features (modules), and reusable code (shared).Group by Domain: Nest business features in
modulesby domain (e.g.,posts,groups) for scalability.Keep
sharedLean: Only place dumb components, pipes, and utilities in thesharedfolder. Business-specific shared code belongs inmodules.Follow Angular v20+ Naming: Drop suffixes for Components, Directives, and Services, and use hyphens for Pipes, Guards, and Interceptors.
Separate Pages: Use a
pagesfolder for routed components to distinguish them from other code.Enforce with ESLint:
Prevent feature-to-feature imports to maintain modularity.
Ensure
coreis 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