Skip to content
← All writingAugust 3, 2026 · 10 min read

How to structure an Angular frontend: core, shared, pages, and component-driven design

  • angular
  • architecture
  • storybook
  • atomic design
  • frontend

Most Angular projects do not collapse under the framework. They collapse under a question nobody can answer six months in: where does this file go?

When there is no answer, everyone invents one. Components pile up in a components/ folder that grows to two hundred entries. Services land wherever the developer who wrote them happened to be. Business rules leak into templates. Nothing is reusable because nothing is findable.

A folder structure is not decoration. It is a set of enforced answers to that question. Here is the one I use, folder by folder, and the reasoning behind each boundary.

The map

src/
  app/
    core/        app-wide singletons: services, guards, interceptors, config
    shared/      reusable and app-agnostic: ui/, directives/, pipes/, utils/
    layout/      shells that wrap routed pages
    pages/       one folder per route
    content/     copy and data, kept out of components
    state/       cross-cutting reactive state only
  styles/        design tokens, reset, mixins, typography

Seven folders. Each one answers a different question, and the test for whether a file belongs is a question you can answer without opening it.

core/ — things there is exactly one of

core/ holds the singletons. One instance, alive for the life of the app, injected wherever needed. No components ever live here.

core/
  config/        constants that describe the environment
  services/      app-wide services (auth, seo, theme, http clients)
  guards/        route guards
  interceptors/  HTTP interceptors
  enums/         shared enumerations
  constants/     shared constant values

The test: would a second instance of this be a bug? An auth service holding the current session, a theme service owning light and dark mode, an SEO service setting meta tags. Two of any of those is a bug, so they belong in core/.

A few that earn their place in most apps:

A platform service. If you server-side render, half your code needs to know whether it is running in a browser. Scattering isPlatformBrowser(inject(PLATFORM_ID)) across thirty files is how SSR bugs get made. Wrap it once:

@Injectable({ providedIn: 'root' })
export class Platform {
  private readonly platformId = inject(PLATFORM_ID);

  readonly isBrowser = isPlatformBrowser(this.platformId);
  readonly prefersReducedMotion = this.isBrowser
    ? window.matchMedia('(prefers-reduced-motion: reduce)').matches
    : false;
}

Now platform.isBrowser is one import instead of a pattern everyone has to remember.

A config folder. API origins, site metadata, feature flags. Constants, not logic. Keeping them in core/config/ means changing an API host is one file, not a search across the repo.

Guards and interceptors in their own folders. Both are small, both are easy to lose inside services/, and both are the first thing you look for when auth misbehaves.

shared/ — reusable, and ignorant of your app

shared/ is everything that could be lifted into a different project without editing it. That is the whole rule, and it is stricter than it sounds.

shared/
  ui/           presentational components (the big one)
  directives/   behaviour attached to elements
  pipes/        template transformations
  utils/        pure functions
  validators/   reusable form validators
  interfaces/   shared type definitions

The test: does this file know anything about your domain? A button does not know what a case study is. A date pipe does not know your business rules. The moment a shared component imports a domain type, it stopped being shared and belongs in a page.

Directives are underused and belong here. Scroll reveals, parallax, magnetic hover, spotlight effects: each is behaviour that any element might want, attached with one attribute, testable in isolation, and reusable across every page.

shared/ui/ — atomic design, and why the layers help

This is where most structures fall apart, because "components" is not a category. Atomic design gives you three, ordered by how much they know.

shared/ui/
  atoms/        cannot be broken down further
  molecules/    a few atoms doing one job
  organisms/    a self-contained section of interface

Atoms are the smallest useful units. A button, an input, a label, a badge, an icon, a heading. They own their appearance and nothing else. An atom never fetches data, never knows a route, and never imports another component from ui/.

Grouping helps once you pass twenty of them:

atoms/
  actions/      button, icon-button, link
  form/         input, label, select, textarea, checkbox, field-error
  display/      badge, tag, avatar, divider, metric
  feedback/     spinner, status-dot, skip-link, scroll-progress
  typography/   heading, text, eyebrow, prose
  media/        image

Molecules combine a few atoms into one job. A form field is a label plus an input plus an error message: three atoms that always travel together. A card is an image plus a heading plus a tag list. Molecules can hold small amounts of presentation logic, but still no data fetching.

Organisms are complete sections. A site header, a footer, a contact form, a mobile nav. They may hold real interaction logic and may talk to a service. This is the layer where an app starts to show through.

The value is not the vocabulary. It is the dependency direction: atoms know nothing, molecules know atoms, organisms know both. Dependencies point one way only. When that holds, you can change a button without auditing the app, because nothing above it reaches back down and grabs its internals.

layout/ — the frame around the page

Layouts are the shells routes render inside. A public shell with header, footer, and mobile nav. An admin shell with its own navigation. An error shell with neither.

layout/
  public-shell/
  admin-layout/
  error-layout/

Keeping these separate from pages/ means the header is defined once, not repeated in every routed component, and a whole surface of the app can be reframed by editing one file.

pages/ — one folder per route

This is the feature layer. In a bigger app you would call it features/, with the same rules.

pages/
  home/
    home.page.ts
    home.page.html
    sections/
      hero-section/
      proof-bar/
      selected-work/
      services-teaser/
      faq-section/
  blog/
    blog-index/
    blog-post/
  work/
    work-index/
    case-study/

Two things make this work.

A page composes, it does not implement. The home page should read as a list of the sections it contains. If a page component is five hundred lines, the sections inside it want to be extracted.

sections/ is deliberately local. A hero section is not reusable, it is this page's hero. Putting it beside the page instead of in shared/ui/ keeps it honest: nobody is tempted to make it configurable for a second caller that does not exist. If a section genuinely gets used twice, that is the signal to promote it into shared/ui/organisms/.

content/ — copy is data, not markup

Headings, intro paragraphs, FAQ entries, service descriptions. Most of it gets hard-coded into templates, and then changing a sentence means editing a component and re-reading its logic.

content/
  models/           the shape of each content type
  home.content.ts
  about.content.ts
  services.content.ts

Typed constants, imported by the page that renders them. Copy changes touch one file with no logic in it. It also makes the eventual move to a CMS or an API a swap of the import rather than a rewrite, because the shape is already defined.

state/ — smaller than you expect

With signals, most state belongs to the service that owns it. A theme service owns the theme. An auth service owns the session. A global store that duplicates them creates two sources of truth that drift.

So state/ holds only what is genuinely cross-cutting and owned by nobody:

export const navOpen = signal(false);
export const isScrolled = signal(false);

Two signals, not a state-management framework. If a piece of state has an obvious owner, it lives with the owner.

styles/ — tokens before components

styles/
  _tokens.scss      colours, spacing, radii, typography scale
  _reset.scss
  _typography.scss
  _mixins.scss
  _utilities.scss
  _motion.scss

_tokens.scss comes first and matters most. Every colour and spacing value in the app resolves to a variable defined there. When a component hard-codes #2f6feb, dark mode becomes a hunt. When it uses a token, dark mode is one file.

Co-location: everything about a component in one folder

button/
  button.ts        logic
  button.html      template
  button.scss      styles
  button.spec.ts   tests
  button.stories.ts  Storybook stories

Five files, one folder, one concept. Deleting a component means deleting a folder. Nothing is stranded in a parallel tests/ or stories/ tree that nobody remembers to clean up.

Storybook and component-driven development

Component-driven development inverts the usual order. Instead of building a page and pulling components out of it, you build components in isolation first and compose the page from things that already work.

Storybook is what makes that practical. Each component gets a .stories.ts file declaring its states:

const meta: Meta<Button> = {
  title: 'Atoms/Button',
  component: Button,
  argTypes: {
    variant: { control: 'select', options: ['primary', 'secondary', 'ghost'] },
    loading: { control: 'boolean' },
  },
};

export const Primary: StoryObj<Button> = { args: { variant: 'primary' } };
export const Loading: StoryObj<Button> = { args: { loading: true } };
export const Disabled: StoryObj<Button> = { args: { disabled: true } };

Three things this buys you.

Edge states get built, not discovered. Loading, disabled, error, empty, and long-text states are cheap to write as stories and expensive to reproduce by clicking through a running app. Most missing empty states exist because nobody could easily get the app into that state.

Isolation exposes hidden dependencies. A component that will not render in Storybook without half the app booted is a component that knows too much. The friction is the feedback.

The catalogue is the design system. New work starts by browsing what exists. That is what stops the fourth slightly-different button from being written.

One scoping decision worth copying: point Storybook at shared/ui/ only.

const config: StorybookConfig = {
  stories: ['../src/app/shared/ui/**/*.stories.@(ts|tsx)'],
  addons: ['@storybook/addon-a11y', '@storybook/addon-docs'],
};

Page sections are not catalogued, because they are not reusable and a catalogue full of one-offs stops being a reference. Add the accessibility addon on day one; catching contrast and label problems per component is far cheaper than an audit at the end.

The rules that keep it honest

Structure decays when the rules live in someone's head. Three worth writing down:

  1. Dependencies point inward and downward. Pages use shared and core. Shared uses nothing from pages. Atoms import no other components. A violation is always a sign something is in the wrong folder.
  2. Shared means domain-free. If it imports a domain type, it is not shared.
  3. Promote on the second use, not the first. Build it local to the page. When a second caller appears, move it up. Designing for reuse before reuse exists produces configurable components with one caller and six unused inputs.

Where to start

You do not need all of this on day one. The order that has worked for me:

  1. styles/_tokens.scss first, before any component.
  2. core/ next: platform, config, and whatever singletons you already know you need.
  3. shared/ui/atoms/ as you need them, with a story for each.
  4. pages/ composing what exists, extracting to sections/ when a page gets long.
  5. Promote to molecules/ and organisms/ when a second caller shows up.

The goal is not a perfect tree. It is that six months from now, "where does this file go?" has one obvious answer, and it is the same answer for everyone on the project.