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.
| 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.
| 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.
Generate an application from this starter:
starter-generator portfolio-web --starter webThen 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:4200Calls to /api are proxied to http://localhost:8080 by the dev proxy (proxy.conf.json): adjust the target to your backend.
| 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 |
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 variablesAnatomy 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/*.
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:
- Replace
openapi/openapi.yamlwith your backend's specification (or pointorval.config.tsat its URL). - Run
pnpm run generate:api. - 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.
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 aspageTitle.*(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 withprovideTranslocoScope('<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.
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>.
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.
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-appThe 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.
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).
- 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
cias the gate job that waits for all the others (the e2e,dockerandopsjobs 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/.servicesuffix, classes named after their role rather than their Angular type, pages suffixed with-page.OnPushis 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
.mjswhere the tool supports it: eslint.config.mjs, prettier.config.mjs, commitlint.config.mjs.
See CONTRIBUTING.md for the contribution workflow and CODE_OF_CONDUCT.md for community guidelines. To report a vulnerability, see SECURITY.md.
This project is licensed under MIT.
