Skip to content
Public template

About

Ready-to-use Angular 22 starter: zoneless, signals, i18n, OpenAPI codegen, module boundaries, unit & e2e tests, CI.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

1 star

Watchers

0 watching

Forks

Latest commit

 

History

162 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Angular Starter Web: ready-to-use Angular starter

CI Release Angular License: MIT

Ready-to-use Angular starter for building a new web application connected to a backend, with every best practice and tool already wired up.

The app ships no global chrome (header, sidebar, navigation): it renders only its body, so it can be embedded in a host shell as-is, or run standalone. No integration mechanism is assumed: it builds, runs and tests on its own either way. It talks to a backend such as java-starter through a client generated from the backend's OpenAPI contract.

The starters

Repository What it generates
angular-starter-web (this one) An Angular web application connected to a backend
angular-starter-ui An Angular design system: component library and Storybook
java-starter A Spring Boot backend in hexagonal architecture

Create an application from any of them with starter-generator.

Tech stack

Tool Role
Angular Framework (standalone, zoneless, signals)
Tailwind CSS Utility-first CSS
ESLint + angular-eslint Lint for TypeScript code and templates
Prettier + sort-imports + tailwindcss Code formatting, import ordering and class sorting
Vitest + Testing Library Unit tests (Angular's default runner)
Playwright End-to-end tests
axe Accessibility audit run inside the e2e suite
Transloco Internationalization (en/fr, runtime language switch)
Orval Generates models and HTTP clients from OpenAPI
Sheriff Enforces module boundaries (core/features/shared)
Husky + lint-staged Git hooks (format + lint on commit)
commitlint Commit message validation (Conventional Commits)
GitHub Actions CI: format, lint, tests, build, e2e
Docker + nginx Production image: multi-stage build served by nginx

Note

The UI component library is not included: plug in the one of your choice.

Getting started

Generate an application from this starter:

starter-generator portfolio-web --starter web

Then run it. Prerequisites: Node (supported versions in engines of package.json) and pnpm.

pnpm install    # installs dependencies and generates the API clients (postinstall)
pnpm start      # dev server on http://localhost:4200

Calls to /api are proxied to http://localhost:8080 by the dev proxy (proxy.conf.json): adjust the target to your backend.

Scripts

Script Description
pnpm start Dev server (with API proxy)
pnpm run build Production build into dist/
pnpm run build:dev Development build into dist/
pnpm test Unit tests (Vitest)
pnpm run test:coverage Unit tests with coverage report and thresholds
pnpm run e2e End-to-end tests (Playwright)
pnpm run e2e:ui E2e tests in interactive mode
pnpm run lint Lint (ESLint)
pnpm run lint:fix Lint with automatic fixes
pnpm run format Format the whole project (Prettier)
pnpm run format:check Check formatting without modifying anything
pnpm run generate:api Regenerates clients and models from the OpenAPI spec

Project structure

Based on the Angular style guide recommendations: grouped by business domain, not by technical type.

src/
|-- app/
|   |-- core/                  # Global, non-business-specific features
|   |   |-- api-client/        # Generated by Orval, do not edit or commit
|   |   |-- i18n/              # Transloco config, LanguageStore, page title strategy
|   |   |-- interceptors/      # HTTP interceptors (error handling, ...)
|   |   |-- logger/            # Logger + LogLevel (only place allowed to call console)
|   |   |-- session/           # SessionStore: what the current user is allowed to do
|   |   `-- not-found-page/    # 404 page
|   |-- features/              # Business features, grouped by domain
|   |   |-- home/              # Single-screen feature
|   |   |   |-- contract/      # home-paths.ts: the only part other modules may import
|   |   |   |-- home-page.ts   # No screen folder: the page sits at the root
|   |   |   `-- home-routes.ts # Every feature owns one, even with a single screen
|   |   `-- users/             # Example of a full feature (lazy loaded, behind an access guard)
|   |       |-- contract/      # users-paths.ts: USERS_PATHS: segments, links, detail(id)
|   |       |-- list/          # Page + its store, and delete-dialog/ with its own
|   |       |-- detail/        # Page + store reading the route parameter
|   |       |-- create/        # Page + store + its signal form
|   |       |-- users-access-guard.ts   # canMatch: redirects when the session says no
|   |       `-- users-routes.ts         # Titles, scope, guard; paths come from the contract
|   `-- shared/                # Reusable code
|       |-- forms/             # Generic form helpers (error display, ...)
|       `-- testing/           # Test utilities
|-- environments/              # Per-environment variables (replaced at build time)
`-- styles.css                 # Global styles: Tailwind import + --app-* CSS variables

Anatomy of a feature: one folder per screen named after its route segment (list/, detail/, create/), plus a <feature>-routes.ts and a contract/. A screen folder holds everything that serves only that screen (page, store, form, dialogs) and the folder carries the short name while the files carry the full one: users/list/user-list-page.ts, class UserListPage. A single-screen feature keeps its page at the root (home/home-page.ts). The contract/ folder is the feature's public surface: pure data (route segments, paths, link builders), no component or service. app-routes.ts imports a feature's routes file and its contract, nothing else, so a feature moves to another app by moving its folder, and no route path is written as a string outside the feature that owns it.

A page never injects the API client: any I/O lives in a <screen>-store.ts beside it, provided by its page component so it is created and destroyed with the screen, see user-list-page.ts. AGENTS.md holds the complete set of rules: where each kind of file goes, how state is scoped, and the recipe for adding a screen.

Dependency rules, enforced at lint time by Sheriff (sheriff.config.ts): features can import core and shared; core can import shared; shared imports neither core nor features; features import each other only through contract/, core may import contracts, and a contract imports nothing from the app; other code shared between features belongs in core or shared. Modules are barrel-less: import files directly (no index.ts), and place files a module wants to keep private in an internal/ subdirectory.

Available import aliases: @core/*, @features/*, @shared/*, @environments/*.

Talking to the backend

The API contract is described in openapi/openapi.yaml. TypeScript models and Angular services are generated by Orval into src/app/core/api-client (gitignored, regenerated on every pnpm install).

To bootstrap a real project:

  1. Replace openapi/openapi.yaml with your backend's specification (or point orval.config.ts at its URL).
  2. Run pnpm run generate:api.
  3. Inject the generated client into the screen's store, never into the page. There is no hand-written pass-through layer: the generated client already is the data-access layer, and the tests mock HTTP, not the service.
// features/users/list/user-list-store.ts
@Injectable()
export class UserListStore {
  #usersApiClient = inject(UsersService);

  readonly search = signal('');
  readonly users = rxResource({
    stream: () => this.#usersApiClient.getUsers(),
    defaultValue: [],
  });

  readonly filteredUsers = computed(() => /* ... */);
}

// features/users/list/user-list-page.ts
@Component({ /* ..., */ providers: [UserListStore] })
export class UserListPage {
  #store = inject(UserListStore);

  protected readonly users = this.#store.users;
  protected readonly filteredUsers = this.#store.filteredUsers;
  protected readonly search = this.#store.search;
}

Add a hand-written <feature>-repository.ts at the feature root only when it earns its place: aggregating several calls into one business operation, mapping DTOs to a view model shared by several pages, or absorbing an API quirk you do not want to spread.

HTTP error handling is centralized in core/interceptors/error-handler-interceptor.ts: it only logs. Showing a failure is the job of the screen that asked (each store owns an error signal), because a toaster raised from the interceptor would duplicate or replace that message.

Access control follows the same path: core/session/session-store.ts asks the backend once what the current user may do, and each feature keeps its own decision in a <feature>-access-guard.ts mounted on its routes file: see users-access-guard.ts.

Internationalization

Translations live in public/i18n/ (en and fr) and are split in two layers:

  • public/i18n/<lang>.json: global keys, preloaded before the app renders. Keep it minimal: only cross-cutting keys that must resolve synchronously, such as pageTitle.* (the title strategy translates on navigation, before any lazy scope has loaded) and the 404 page.
  • public/i18n/<feature>/<lang>.json: one scope per feature, declared with provideTranslocoScope('<feature>') in the feature's routes and fetched lazily alongside it. Keys are read with the scope as prefix:
<h1>{{ 'users.list.title' | transloco }}</h1>

The language can be switched at runtime via LanguageStore.setActiveLang(), by the app itself, or by a host shell embedding it, and is persisted in a cookie. Page titles are translated and suffixed automatically by core/i18n/title-strategy.ts.

Styling

Tailwind CSS v4 is wired up via PostCSS (.postcssrc.json) and imported once in src/styles.css with @import 'tailwindcss';. No tailwind.config.js is needed for basic usage (Tailwind v4 is CSS-first); use @theme in styles.css to customize tokens if needed. Components carry no stylesheet of their own, utilities in the template and @theme tokens cover it, but nothing prevents adding a styleUrl if one ever needs it. The active color scheme follows the OS preference, unless a host shell forces one by setting data-theme on <html>.

Environments

src/environments/environment.ts holds local development values and is replaced at build time by environment.production.ts (see fileReplacements in angular.json). Always import @environments/environment, never a specific file.

Deployment

Dockerfile builds the production image in two stages: Node installs the dependencies, generates the API client and builds the app; nginx serves the result on port 80.

docker build -t my-app .
docker run --rm -p 8080:80 my-app

The nginx config does the two things a single-page app needs: unmatched paths fall back to index.html so a refresh on /users/1 still works, and hashed assets are cached for a year while everything else is revalidated. A missing file answers 404, and nginx also sends the security headers (a strict CSP with Trusted Types, frame-ancestors 'none'). Calls to /api are not proxied there, proxy.conf.json is a dev-server concern only. In production a host shell or an ingress routes them; add a location /api to nginx.conf if the container has to reach the backend itself. The strict CSP forbids embedding the app in an iframe and writing HTML from a UI library: relax frame-ancestors and X-Frame-Options for an iframe host, and add a named Trusted Types policy for a library.

Testing

Component tests use Angular Testing Library (render, screen, userEvent): querying by role or label asserts accessibility for free and matches the Playwright getByRole style used in e2e. Stores, interceptors and form schemas are tested with plain TestBed, without rendering a template. The jest-dom matchers (toBeInTheDocument, toBeEnabled, ...) are registered in src/test-setup.ts.

E2e tests live in e2e/ (pages/ for page objects, fixtures/ for custom test fixtures), next to the app rather than in a separate package. They also run axe on the rendered page through expectNoAccessibilityViolations, which fails on any WCAG 2.1 A/AA violation (call it again once a dialog is open, since axe only sees what is currently rendered).

Coverage counts every source file, not only the ones a spec imports: coverageInclude in angular.json is what makes an untested file report as uncovered instead of disappearing from the report. coverageExclude holds the generated client and the pure wiring (bootstrap, *-routes.ts, *-provider.ts, environments).

Quality and conventions

  • On commit: lint-staged formats and lints the staged files; commitlint enforces Conventional Commits (feat: ..., fix: ..., ...).
  • In CI (.github/workflows/ci.yml): format check, lint, unit tests, build and e2e on every push/PR, with the required check ci as the gate job that waits for all the others (the e2e, docker and ops jobs included). A separate workflow, generator-check.yml, scaffolds an app from the working tree and runs its own gates, so the generator manifest and templates cannot drift (maintainer notes in .generator/README.md).
  • Code conventions: signals, inject(), # private fields, control flow (@if, @for), no .component/.service suffix, classes named after their role rather than their Angular type, pages suffixed with -page. OnPush is Angular 22's default and is never declared. The full list lives in AGENTS.md, next to the Angular style guide.
  • Config files use ESM .mjs where the tool supports it: eslint.config.mjs, prettier.config.mjs, commitlint.config.mjs.

Contributing

See CONTRIBUTING.md for the contribution workflow and CODE_OF_CONDUCT.md for community guidelines. To report a vulnerability, see SECURITY.md.

License

This project is licensed under MIT.

About

Ready-to-use Angular 22 starter: zoneless, signals, i18n, OpenAPI codegen, module boundaries, unit & e2e tests, CI.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages