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.

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:

  1. Is it loaded directly by a route? Put it in features/<feature>/pages.
  2. Is it used only by one feature? Put it inside that feature.
  3. Is it app-wide infrastructure? Put it in core.
  4. Is it reused across apps or product areas? Move it into a library.
  5. 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.

  1. Map current routes. List every route target and group them by business capability.
  2. Create feature folders. Start with the most active or most confusing product areas.
  3. Move route targets into pages. Keep imports working; do not change behavior yet.
  4. Move feature-owned services/components beside the feature. Leave app-wide infrastructure in core.
  5. Extract reusable primitives into libraries. Only do this once usage is proven across boundaries.
  6. Add barrel exports where directories expose public symbols. Keep imports stable and readable.
  7. 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.