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

Table of contents
- Why Folder Structure and ESLint Rules Matter
- Key Changes in Angular v20+
- Types of Code in Your Application
- 1. Non-Business Features (core Folder)
- 2. Business Features (features or modules Folder)
- 3. Shared Code (shared Folder)
- Complete Folder Structure Example
- Setting Up ESLint in Your Angular Project
- Key Takeaways

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 nowLogin
,auth.service.ts
is nowauth.ts
).Pipes, Guards, Resolvers, Interceptors, and Modules retain suffixes but use a hyphen instead of a dot (e.g.,
auth-guard.ts
instead 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 apages
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 inapp.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:
Prevent Imports from
features
ormodules
:The
core
folder should not depend on business features to remain self-contained and reusable.Use the
no-restricted-imports
rule to block imports fromfeatures
ormodules
.
{
"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
core
module should be eagerly loaded inapp.routes.ts
orapp.config.ts
. While ESLint can’t directly enforce eager loading, you can use a custom rule to ensurecore
files are not referenced in lazy-loaded routes.Alternatively, document that
core
routes 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
core
follow Angular v20 naming conventions (e.g.,auth-guard.ts
, no suffixes for services likeauth-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:
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."
}
]
}
]
}
}
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 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-syntax
rule from thecore
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:
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."
}
]
}
]
}
}
Enforce Naming Conventions:
Ensure shared components, pipes, and utilities follow Angular v20 naming conventions.
Reuse the
no-restricted-syntax
rule from thecore
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:
Install ESLint and Angular ESLint Plugin:
npm install eslint @angular-eslint/schematics --save-dev
Initialize ESLint: Run the following command to set up ESLint in your Angular project:
ng add @angular-eslint/schematics
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+." } ] } } ] }
Run ESLint: Check your code for violations:
ng lint
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
, andshared
: 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 theshared
folder. 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
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
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