Angular Folder Structure Hierarchy Stable
Angular applications should be organized around routes and business capabilities first, then technical type. A folder structure is not just a filing system; it is an ownership model. When the structure mirrors the product, teams know where code belongs, where change impact lives, and when a piece of functionality has become reusable enough to move into a library.
This pattern is designed for modern standalone Angular applications, especially Nx monorepos where reusable UI, services, and domain primitives can graduate into shared libraries instead of collecting in an app-level junk drawer.
Recommended Shape
The default structure is route-first and feature-sliced:
src/app/
├── app.config.ts
├── app.routes.ts
├── app.component.ts
│
├── layouts/
│ └── main-layout/
│ ├── main-layout.component.ts
│ ├── main-layout.component.html
│ └── main-layout.component.scss
│
├── core/
│ ├── guards/
│ ├── interceptors/
│ ├── services/
│ ├── tokens/
│ └── schemas/
│
├── features/
│ └── users/
│ ├── users.routes.ts
│ ├── pages/
│ │ ├── users-list/
│ │ ├── user-detail/
│ │ └── user-form/
│ ├── components/
│ ├── services/
│ ├── models/
│ ├── schemas/
│ ├── guards/
│ └── utils/
│
└── pages/ # optional: tiny app-lifecycle routes only
└── auth-callback/
└── callback.component.ts
The important part is not memorizing these folder names. The important part is the hierarchy of responsibility:
app composition at the root, app-wide infrastructure in core, business areas in features,
and reusable cross-app code in libraries.
The default home for route pages is features/<feature>/pages. A top-level pages
folder is only an escape hatch for tiny app-lifecycle routes that do not yet have durable product ownership,
such as a one-screen OAuth callback. If that area grows into login, signup, MFA, recovery, sessions, or related
services, promote it to features/auth/pages.
Hierarchy Rules
1. Keep the app root boring
The root of src/app should compose the application. It should not become a dumping ground for
business logic.
src/app/
├── app.config.ts # providers, app-level configuration
├── app.routes.ts # top-level route composition
└── app.component.ts # root shell only If a file in the app root does not participate in bootstrapping, routing, or root composition, it probably belongs somewhere else.
2. Use core for app-wide infrastructure
core is for singleton behavior and infrastructure that applies across the whole app:
- authentication and authorization guards
- HTTP interceptors
- app session services
- global injection tokens
- current-user or app-context schemas
core/
├── guards/auth.guard.ts
├── interceptors/api.interceptor.ts
├── services/session.service.ts
├── tokens/api-url.token.ts
└── schemas/current-user.schema.ts
Do not put a feature service in core just because two pages use it. If the concept belongs to a
business capability, keep it with that feature until it has truly become app-wide infrastructure.
3. Let features own business capabilities
A feature folder represents a business capability or routed product area: users,
billing, workspaces, settings, projects, reports.
features/workspaces/
├── workspaces.routes.ts
├── pages/
│ ├── workspaces-list/
│ ├── workspace-detail/
│ └── workspace-form/
├── components/
│ ├── workspace-status-badge/
│ └── workspace-member-picker/
├── services/
│ └── workspaces.service.ts
├── schemas/
│ └── workspace.schemas.ts
└── models/
└── workspace.model.ts
This creates a useful boundary: if you are changing the workspaces product area, most of your changes should
happen under features/workspaces. If they do not, that is architectural feedback.
4. Route targets go in feature-owned pages
A component loaded directly by the router is a page. Pages compose smaller components and coordinate route-level behavior. By default, those pages live under the feature that owns the route.
export const appRoutes: Route[] = [
{
path: 'workspaces',
loadChildren: () =>
import('./features/workspaces/workspaces.routes').then((m) => m.workspacesRoutes),
},
]; export const workspacesRoutes: Route[] = [
{
path: '',
loadComponent: () =>
import('./pages/workspaces-list').then((m) => m.WorkspacesListComponent),
},
{
path: ':id',
loadComponent: () =>
import('./pages/workspace-detail').then((m) => m.WorkspaceDetailComponent),
},
]; A reusable table, picker, badge, dialog, or panel is not a page unless the router loads it as one.
Use top-level pages only for small app-lifecycle routes that are not a real product feature yet.
The moment that area has multiple screens, services, guards, models, or business rules, it belongs under
features/<feature>/pages.
5. Keep components feature-local until proven reusable
The default place for a component is next to the feature that owns it:
features/users/components/assign-role-dialog/
features/users/components/user-status-badge/
Move it to a shared library only when it is reused across multiple features or apps and its API has stabilized.
In a monorepo, that usually means a library such as libs/pixel, not an app-level
shared folder.
6. Keep services beside their domain
Feature-specific services live with their feature:
features/billing/services/billing.service.ts
features/users/services/users.service.ts App-wide infrastructure services live in core:
core/services/auth-session.service.ts
core/services/app-context.service.ts Cross-app reusable services graduate into libraries:
libs/pixel/src/lib/admin/services/dialog.service.ts
libs/pixel/src/lib/butler/services/update.service.ts 7. Split feature routes when the top-level router stops being readable
Small apps can keep everything in app.routes.ts. Larger apps should let each feature own its child
routes:
features/settings/settings.routes.ts
features/users/users.routes.ts
features/billing/billing.routes.ts This keeps top-level routing focused on product areas instead of every individual screen.
8. Avoid Angular modules for new code by default
For modern standalone Angular, prefer standalone components and route files over feature modules:
# Prefer
features/users/users.routes.ts
features/users/pages/users-list/users-list.component.ts
# Avoid for new standalone code
modules/users/users.module.ts
modules/users/users-routing.module.ts Keep modules when integrating legacy Angular code or a third-party library that still expects module-based configuration. Do not create modules just because older Angular apps did.
Decision Tree
When adding a file, ask these questions in order:
- Is it loaded directly by a route? Put it in
features/<feature>/pages. - Is it used only by one feature? Put it inside that feature.
- Is it app-wide infrastructure? Put it in
core. - Is it reused across apps or product areas? Move it into a library.
- Is it only “shared” because you are unsure? Keep it feature-local until evidence says otherwise.
| File | Recommended Location | Why |
|---|---|---|
auth.guard.ts | core/guards | App-wide routing infrastructure |
users.service.ts | features/users/services | Owned by the users domain |
user-status-badge.component.ts | features/users/components | Feature-local UI until reused elsewhere |
data-table.component.ts | libs/pixel/src/lib/.../components | Reusable cross-feature/cross-app primitive |
workspace-detail.component.ts | features/workspaces/pages | Route target for workspace detail |
Anti-Patterns
The top-level technical junk drawer
src/app/
├── components/
├── services/
├── models/
├── utils/
└── types/
This looks organized because the nouns are familiar. It scales poorly because ownership disappears. A developer
changing billing should not need to inspect a global services directory to discover whether a billing
service lives there, in a module, or under a page.
The permanent shared folder
shared is often a polite name for “nobody owns this.” Use it sparingly. If code is truly reusable,
graduate it into a library with a clear public API. If it is not, keep it with the feature that owns it.
One module per feature by reflex
Feature modules were useful in older Angular architecture. Standalone Angular gives us a simpler path: route files, standalone components, and explicit lazy loading. Do not carry module ceremony forward unless it solves a current problem.
Pages that behave like reusable components
If a page is imported directly by another page to reuse UI, split the reusable UI into a feature component. Pages should coordinate route-level concerns; components should encapsulate reusable presentation and behavior.
Migration Path
For an existing Angular app, migrate gradually. Folder structure refactors should reduce confusion, not create a ceremonial rewrite.
- Map current routes. List every route target and group them by business capability.
- Create feature folders. Start with the most active or most confusing product areas.
- Move route targets into
pages. Keep imports working; do not change behavior yet. - Move feature-owned services/components beside the feature. Leave app-wide infrastructure in
core. - Extract reusable primitives into libraries. Only do this once usage is proven across boundaries.
- Add barrel exports where directories expose public symbols. Keep imports stable and readable.
- Delete empty junk drawers. A migration is not done until old ambiguous folders stop attracting new code.
Summary
The best Angular folder structure is not the one with the most categories. It is the one that makes ownership obvious. Start from routes and business capabilities, keep infrastructure separate, keep feature code together, and promote reusable code into libraries only when reuse is real.