Scaffold an opinionated Angular 19 app — standalone components, Signals, Angular Material, Tailwind, strict TypeScript, Vitest, Husky
Scaffold a production-grade Angular 19 application. Follow every instruction below exactly. Do not skip steps. Do not ask before proceeding — just build it.
Project name: $ARGUMENTS
inject() function for all dependency injection — no constructor injectionstrict, strictTemplates, noImplicitOverride, exactOptionalPropertyTypesnpx @angular/cli@latest new {name} \
--standalone \
--routing \
--style scss \
--strict \
--skip-git \
--package-manager npm
cd {name}
npm install tailwindcss @tailwindcss/vite --save-dev
Add to vite.config.ts (create if needed for Vitest, see Step 4):
import tailwindcss from '@tailwindcss/vite';
Add to src/styles.scss:
@import "tailwindcss";
ng add @angular/material
# Choose: Indigo/Pink theme, set up typography, include animations
npm uninstall karma karma-chrome-launcher karma-coverage karma-jasmine karma-jasmine-html-reporter jasmine-core @types/jasmine
npm install vitest @vitest/coverage-v8 @analogjs/vite-plugin-angular jsdom --save-dev
Create vitest.config.ts:
import { defineConfig } from 'vitest/config';
import angular from '@analogjs/vite-plugin-angular';
export default defineConfig({
plugins: [angular()],
test: {
globals: true,
environment: 'jsdom',
setupFiles: ['src/test-setup.ts'],
include: ['src/**/*.spec.ts'],
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html'],
exclude: ['node_modules/', 'src/test-setup.ts'],
thresholds: {
lines: 80,
functions: 80,
branches: 80,
statements: 80,
},
},
},
});
Create src/test-setup.ts:
import '@angular/compiler';
import { TestBed } from '@angular/core/testing';
import { provideNoopAnimations } from '@angular/platform-browser/animations';
beforeEach(() => {
TestBed.configureTestingModule({
providers: [provideNoopAnimations()],
});
});
Update package.json scripts:
{
"scripts": {
"test": "vitest run",
"test:watch": "vitest",
"test:coverage": "vitest run --coverage"
}
}
ng add @angular-eslint/schematics
npm install prettier prettier-plugin-organize-imports --save-dev
.eslintrc.json (replace generated):
{
"root": true,
"ignorePatterns": ["projects/**/*"],
"overrides": [
{
"files": ["*.ts"],
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/strict-type-checked",
"plugin:@angular-eslint/recommended",
"plugin:@angular-eslint/template/process-inline-templates"
],
"parserOptions": {
"project": ["tsconfig.json"],
"createDefaultProgram": true
},
"rules": {
"@angular-eslint/directive-selector": ["error", { "type": "attribute", "prefix": "app", "style": "camelCase" }],
"@angular-eslint/component-selector": ["error", { "type": "element", "prefix": "app", "style": "kebab-case" }],
"@angular-eslint/prefer-standalone": "error",
"@typescript-eslint/explicit-function-return-type": "error",
"@typescript-eslint/no-explicit-any": "error",
"@typescript-eslint/no-unused-vars": "error",
"@typescript-eslint/prefer-readonly": "error",
"no-console": "warn"
}
},
{
"files": ["*.html"],
"extends": [
"plugin:@angular-eslint/template/recommended",
"plugin:@angular-eslint/template/accessibility"
],
"rules": {}
}
]
}
.prettierrc:
{
"singleQuote": true,
"trailingComma": "all",
"printWidth": 100,
"tabWidth": 2,
"semi": true,
"plugins": ["prettier-plugin-organize-imports"]
}
.prettierignore:
dist/
.angular/
node_modules/
npm install husky lint-staged --save-dev
npx husky init
.husky/pre-commit:
npx lint-staged
package.json (add):
{
"lint-staged": {
"*.{ts,html}": ["eslint --fix", "prettier --write"],
"*.{scss,css,json,md}": ["prettier --write"]
}
}
tsconfig.json:
{
"compileOnSave": false,
"compilerOptions": {
"outDir": "./dist/out-tsc",
"forceConsistentCasingInFileNames": true,
"strict": true,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"exactOptionalPropertyTypes": true,
"useUnknownInCatchVariables": true,
"skipLibCheck": true,
"esModuleInterop": true,
"sourceMap": true,
"declaration": false,
"experimentalDecorators": true,
"moduleResolution": "bundler",
"importHelpers": true,
"target": "ES2022",
"module": "ES2022",
"lib": ["ES2022", "dom"],
"paths": {
"@core/*": ["src/app/core/*"],
"@features/*": ["src/app/features/*"],
"@shared/*": ["src/app/shared/*"],
"@env/*": ["src/environments/*"]
}
},
"angularCompilerOptions": {
"enableI18nLegacyMessageIdFormat": false,
"strictInjectionParameters": true,
"strictInputAccessModifiers": true,
"strictTemplates": true
}
}
Create this structure:
src/app/
├── core/
│ ├── interceptors/
│ │ ├── auth.interceptor.ts
│ │ ├── error.interceptor.ts
│ │ └── loading.interceptor.ts
│ ├── guards/
│ │ └── auth.guard.ts
│ └── services/
│ └── auth.service.ts
├── shared/
│ ├── components/
│ ├── directives/
│ ├── pipes/
│ └── models/
├── features/
│ └── home/
│ ├── home.component.ts
│ ├── home.component.html
│ ├── home.component.scss
│ └── home.component.spec.ts
├── layout/
│ ├── shell/
│ │ └── shell.component.ts
│ ├── header/
│ │ └── header.component.ts
│ └── footer/
│ └── footer.component.ts
├── app.component.ts
├── app.config.ts
└── app.routes.ts
app.config.ts:
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
import { provideRouter, withComponentInputBinding, withViewTransitions } from '@angular/router';
import { provideHttpClient, withInterceptors, withFetch } from '@angular/common/http';
import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';
import { routes } from './app.routes';
import { authInterceptor } from './core/interceptors/auth.interceptor';
import { errorInterceptor } from './core/interceptors/error.interceptor';
export const appConfig: ApplicationConfig = {
providers: [
provideZoneChangeDetection({ eventCoalescing: true }),
provideRouter(routes, withComponentInputBinding(), withViewTransitions()),
provideHttpClient(
withFetch(),
withInterceptors([authInterceptor, errorInterceptor])
),
provideAnimationsAsync(),
],
};
app.routes.ts:
import { Routes } from '@angular/router';
export const routes: Routes = [
{
path: '',
loadComponent: () =>
import('./features/home/home.component').then((m) => m.HomeComponent),
},
{ path: '**', redirectTo: '' },
];
core/interceptors/auth.interceptor.ts:
import { HttpInterceptorFn } from '@angular/common/http';
import { inject } from '@angular/core';
import { AuthService } from '../services/auth.service';
export const authInterceptor: HttpInterceptorFn = (req, next) => {
const authService = inject(AuthService);
const token = authService.token();
if (token) {
const authReq = req.clone({
headers: req.headers.set('Authorization', `Bearer ${token}`),
});
return next(authReq);
}
return next(req);
};
core/interceptors/error.interceptor.ts:
import { HttpInterceptorFn, HttpErrorResponse } from '@angular/common/http';
import { inject } from '@angular/core';
import { catchError, throwError } from 'rxjs';
export const errorInterceptor: HttpInterceptorFn = (req, next) => {
return next(req).pipe(
catchError((error: HttpErrorResponse) => {
console.error(`HTTP ${error.status}: ${error.message}`);
return throwError(() => error);
})
);
};
core/services/auth.service.ts:
import { Injectable, signal } from '@angular/core';
@Injectable({ providedIn: 'root' })
export class AuthService {
readonly token = signal<string | null>(null);
readonly isAuthenticated = signal(false);
setToken(token: string): void {
this.token.set(token);
this.isAuthenticated.set(true);
}
clearToken(): void {
this.token.set(null);
this.isAuthenticated.set(false);
}
}
src/environments/environment.ts:
export const environment = {
production: false,
apiUrl: 'http://localhost:8080',
};
src/environments/environment.prod.ts:
export const environment = {
production: true,
apiUrl: '',
};
{
"scripts": {
"start": "ng serve",
"build": "ng build",
"build:prod": "ng build --configuration production",
"test": "vitest run",
"test:watch": "vitest",
"test:coverage": "vitest run --coverage",
"lint": "ng lint",
"lint:fix": "ng lint --fix",
"format": "prettier --write \"src/**/*.{ts,html,scss,json}\"",
"format:check": "prettier --check \"src/**/*.{ts,html,scss,json}\""
}
}
ng build succeeds with zero errors or warningsnpm test passesnpm run lint passes with zero errorsinject() used everywhere — no constructor injection