TypeScript Angular Basic
A comprehensive Angular application archetype built with TypeScript, providing a modern, scalable foundation for enterprise web applications with best practices, testing frameworks, and deployment configurations.
Overview
This archetype generates a complete Angular application using the latest Angular framework features, TypeScript for type safety, and a comprehensive development environment. It includes modern tooling, testing frameworks, and production-ready configurations for enterprise applications.
Technology Stack
- Angular 17+: Latest Angular framework with signals and standalone components
- TypeScript 5+: Strong typing and modern JavaScript features
- Angular CLI: Command-line interface for development and build processes
- RxJS: Reactive programming with observables
- Angular Material: Material Design component library
- Jest: Testing framework with extensive mocking capabilities
- Cypress: End-to-end testing framework
- ESLint: Code linting and style enforcement
- Prettier: Code formatting and consistency
- Husky: Git hooks for quality assurance
Key Features
Core Functionality
- Component Architecture: Modular, reusable component design
- Reactive Forms: Type-safe form handling with validation
- HTTP Client: RESTful API integration with interceptors
- Routing: Advanced routing with guards and lazy loading
- State Management: NgRx for complex state management (optional)
Development Features
- Hot Module Replacement: Fast development with live reloading
- TypeScript Configuration: Strict typing and modern ES features
- Component Library: Shared component library structure
- Service Architecture: Injectable services with dependency injection
- Interceptors: HTTP request/response interceptors
Production Features
- Lazy Loading: Code splitting for optimal performance
- Tree Shaking: Dead code elimination for smaller bundles
- Progressive Web App: PWA capabilities with service workers
- Internationalization: i18n support for multiple languages
- Security: XSS protection and Content Security Policy
Project Structure
{{ project-name }}/
├── src/
│ ├── app/
│ │ ├── app.component.css # Root component styles
│ │ ├── app.component.html # Root component template
│ │ ├── app.component.spec.ts # Root component tests
│ │ ├── app.component.ts # Root component logic
│ │ ├── app.config.ts # Application configuration
│ │ └── app.routes.ts # Application routes
│ ├── index.html # Main HTML file
│ ├── main.ts # Application bootstrap
│ └── styles.css # Global styles
├── public/ # Static assets
│ ├── app.svg # App logo
│ ├── favicon.ico # Favicon
│ ├── globe.svg # Icon assets
│ ├── learn.svg
│ ├── rocket.svg
│ └── ybor-logo.svg
├── angular.json # Angular CLI configuration
├── Dockerfile # Container configuration
├── package.json # Dependencies and scripts
├── pnpm-lock.yaml # Lock file
├── README.md # Project documentation
├── tsconfig.app.json # App TypeScript config
├── tsconfig.json # Base TypeScript config
└── tsconfig.spec.json # Test TypeScript config
Component Architecture
Standalone Components
// Modern Angular standalone component
import { Component, input, output, computed } from '@angular/core';
import { CommonModule } from '@angular/common';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
@Component({
selector: 'app-user-card',
standalone: true,
imports: [CommonModule, MatButtonModule, MatCardModule],
template: `
<mat-card class="user-card">
<mat-card-header>
<mat-card-title>{{ displayName() }}</mat-card-title>
<mat-card-subtitle>{{ user().email }}</mat-card-subtitle>
</mat-card-header>
<mat-card-content>
<p>{{ user().bio }}</p>
<div class="user-stats">
<span>Posts: {{ user().postsCount }}</span>
<span>Followers: {{ user().followersCount }}</span>
</div>
</mat-card-content>
<mat-card-actions>
<button mat-button (click)="onFollow()">
{{ isFollowing() ? 'Unfollow' : 'Follow' }}
</button>
<button mat-button (click)="onMessage()">Message</button>
</mat-card-actions>
</mat-card>
`,
styleUrls: ['./user-card.component.scss']
})
export class UserCardComponent {
// Inputs using new signal-based APIs
user = input.required<User>();
isFollowing = input<boolean>(false);
// Outputs using new signal-based APIs
follow = output<User>();
message = output<User>();
// Computed properties
displayName = computed(() => {
const user = this.user();
return `${user.firstName} ${user.lastName}`;
});
onFollow(): void {
this.follow.emit(this.user());
}
onMessage(): void {
this.message.emit(this.user());
}
}
// User interface
export interface User {
id: string;
firstName: string;
lastName: string;
email: string;
bio?: string;
postsCount: number;
followersCount: number;
avatarUrl?: string;
}
Service Architecture
// Core user service with dependency injection
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, BehaviorSubject, catchError, map, tap } from 'rxjs';
import { environment } from '../../environments/environment';
@Injectable({
providedIn: 'root'
})
export class UserService {
private readonly http = inject(HttpClient);
private readonly apiUrl = `${environment.apiUrl}/users`;
// State management with signals (optional)
private readonly usersSubject = new BehaviorSubject<User[]>([]);
public readonly users$ = this.usersSubject.asObservable();
private readonly currentUserSubject = new BehaviorSubject<User | null>(null);
public readonly currentUser$ = this.currentUserSubject.asObservable();
getUsers(): Observable<User[]> {
return this.http.get<ApiResponse<User[]>>(`${this.apiUrl}`)
.pipe(
map(response => response.data),
tap(users => this.usersSubject.next(users)),
catchError(this.handleError)
);
}
getUserById(id: string): Observable<User> {
return this.http.get<ApiResponse<User>>(`${this.apiUrl}/${id}`)
.pipe(
map(response => response.data),
catchError(this.handleError)
);
}
createUser(userData: CreateUserRequest): Observable<User> {
return this.http.post<ApiResponse<User>>(this.apiUrl, userData)
.pipe(
map(response => response.data),
tap(user => {
const currentUsers = this.usersSubject.value;
this.usersSubject.next([...currentUsers, user]);
}),
catchError(this.handleError)
);
}
updateUser(id: string, userData: UpdateUserRequest): Observable<User> {
return this.http.put<ApiResponse<User>>(`${this.apiUrl}/${id}`, userData)
.pipe(
map(response => response.data),
tap(updatedUser => {
const currentUsers = this.usersSubject.value;
const updatedUsers = currentUsers.map(user =>
user.id === id ? updatedUser : user
);
this.usersSubject.next(updatedUsers);
}),
catchError(this.handleError)
);
}
deleteUser(id: string): Observable<void> {
return this.http.delete<void>(`${this.apiUrl}/${id}`)
.pipe(
tap(() => {
const currentUsers = this.usersSubject.value;
const filteredUsers = currentUsers.filter(user => user.id !== id);
this.usersSubject.next(filteredUsers);
}),
catchError(this.handleError)
);
}
private handleError(error: any): Observable<never> {
console.error('An error occurred:', error);
throw error;
}
}
// API response interfaces
interface ApiResponse<T> {
data: T;
message: string;
success: boolean;
}
interface CreateUserRequest {
firstName: string;
lastName: string;
email: string;
bio?: string;
}
interface UpdateUserRequest {
firstName?: string;
lastName?: string;
email?: string;
bio?: string;
}
Reactive Forms
// Form component with reactive forms and validation
import { Component, inject, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ReactiveFormsModule, FormBuilder, FormGroup, Validators } from '@angular/forms';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { MatButtonModule } from '@angular/material/button';
import { MatSelectModule } from '@angular/material/select';
@Component({
selector: 'app-user-form',
standalone: true,
imports: [
CommonModule,
ReactiveFormsModule,
MatFormFieldModule,
MatInputModule,
MatButtonModule,
MatSelectModule
],
template: `
<form [formGroup]="userForm" (ngSubmit)="onSubmit()" class="user-form">
<mat-form-field appearance="outline">
<mat-label>First Name</mat-label>
<input matInput formControlName="firstName" placeholder="Enter first name">
<mat-error *ngIf="userForm.get('firstName')?.hasError('required')">
First name is required
</mat-error>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Last Name</mat-label>
<input matInput formControlName="lastName" placeholder="Enter last name">
<mat-error *ngIf="userForm.get('lastName')?.hasError('required')">
Last name is required
</mat-error>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Email</mat-label>
<input matInput type="email" formControlName="email" placeholder="Enter email">
<mat-error *ngIf="userForm.get('email')?.hasError('required')">
Email is required
</mat-error>
<mat-error *ngIf="userForm.get('email')?.hasError('email')">
Please enter a valid email
</mat-error>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Bio</mat-label>
<textarea matInput formControlName="bio" placeholder="Tell us about yourself" rows="3"></textarea>
</mat-form-field>
<div class="form-actions">
<button mat-button type="button" (click)="onReset()">Reset</button>
<button mat-raised-button color="primary" type="submit" [disabled]="!userForm.valid">
{{ isEditMode ? 'Update' : 'Create' }} User
</button>
</div>
</form>
`,
styleUrls: ['./user-form.component.scss']
})
export class UserFormComponent implements OnInit {
private readonly fb = inject(FormBuilder);
userForm!: FormGroup;
isEditMode = false;
ngOnInit(): void {
this.initializeForm();
}
private initializeForm(): void {
this.userForm = this.fb.group({
firstName: ['', [Validators.required, Validators.minLength(2)]],
lastName: ['', [Validators.required, Validators.minLength(2)]],
email: ['', [Validators.required, Validators.email]],
bio: ['', [Validators.maxLength(500)]]
});
}
onSubmit(): void {
if (this.userForm.valid) {
const formValue = this.userForm.value;
console.log('Form submitted:', formValue);
// Handle form submission
if (this.isEditMode) {
this.updateUser(formValue);
} else {
this.createUser(formValue);
}
} else {
this.markFormGroupTouched();
}
}
onReset(): void {
this.userForm.reset();
}
private createUser(userData: any): void {
// Implementation for creating user
console.log('Creating user:', userData);
}
private updateUser(userData: any): void {
// Implementation for updating user
console.log('Updating user:', userData);
}
private markFormGroupTouched(): void {
Object.keys(this.userForm.controls).forEach(key => {
const control = this.userForm.get(key);
control?.markAsTouched();
});
}
}
Routing & Navigation
Application Routes
// Modern Angular routing configuration
import { Routes } from '@angular/router';
import { AuthGuard } from './core/guards/auth.guard';
import { AdminGuard } from './core/guards/admin.guard';
export const routes: Routes = [
{
path: '',
redirectTo: '/dashboard',
pathMatch: 'full'
},
{
path: 'auth',
loadChildren: () => import('./features/auth/auth.routes').then(m => m.authRoutes)
},
{
path: 'dashboard',
loadComponent: () => import('./features/dashboard/dashboard.component').then(m => m.DashboardComponent),
canActivate: [AuthGuard]
},
{
path: 'users',
loadChildren: () => import('./features/users/user.routes').then(m => m.userRoutes),
canActivate: [AuthGuard]
},
{
path: 'admin',
loadChildren: () => import('./features/admin/admin.routes').then(m => m.adminRoutes),
canActivate: [AuthGuard, AdminGuard]
},
{
path: 'profile',
loadComponent: () => import('./features/profile/profile.component').then(m => m.ProfileComponent),
canActivate: [AuthGuard]
},
{
path: '**',
loadComponent: () => import('./shared/components/not-found/not-found.component').then(m => m.NotFoundComponent)
}
];
Route Guards
// Authentication guard
import { inject } from '@angular/core';
import { Router } from '@angular/router';
import { AuthService } from '../services/auth.service';
export const AuthGuard = () => {
const authService = inject(AuthService);
const router = inject(Router);
if (authService.isAuthenticated()) {
return true;
}
router.navigate(['/auth/login']);
return false;
};
// Admin guard
export const AdminGuard = () => {
const authService = inject(AuthService);
const router = inject(Router);
if (authService.hasRole('admin')) {
return true;
}
router.navigate(['/dashboard']);
return false;
};
Testing Strategy
Unit Testing with Jest
// Component unit test
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ReactiveFormsModule } from '@angular/forms';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { UserFormComponent } from './user-form.component';
describe('UserFormComponent', () => {
let component: UserFormComponent;
let fixture: ComponentFixture<UserFormComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [
UserFormComponent,
ReactiveFormsModule,
NoopAnimationsModule,
MatFormFieldModule,
MatInputModule
]
}).compileComponents();
fixture = TestBed.createComponent(UserFormComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should initialize form with empty values', () => {
expect(component.userForm.get('firstName')?.value).toBe('');
expect(component.userForm.get('lastName')?.value).toBe('');
expect(component.userForm.get('email')?.value).toBe('');
});
it('should mark form as invalid when required fields are empty', () => {
expect(component.userForm.valid).toBeFalsy();
});
it('should mark form as valid when all required fields are filled', () => {
component.userForm.patchValue({
firstName: 'John',
lastName: 'Doe',
email: 'john.doe@example.com'
});
expect(component.userForm.valid).toBeTruthy();
});
it('should emit form data on valid submission', () => {
spyOn(component, 'createUser');
component.userForm.patchValue({
firstName: 'John',
lastName: 'Doe',
email: 'john.doe@example.com'
});
component.onSubmit();
expect(component.createUser).toHaveBeenCalledWith({
firstName: 'John',
lastName: 'Doe',
email: 'john.doe@example.com',
bio: ''
});
});
});
Service Testing
// Service unit test
import { TestBed } from '@angular/core/testing';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { UserService } from './user.service';
import { User } from '../models/user.model';
describe('UserService', () => {
let service: UserService;
let httpMock: HttpTestingController;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
providers: [UserService]
});
service = TestBed.inject(UserService);
httpMock = TestBed.inject(HttpTestingController);
});
afterEach(() => {
httpMock.verify();
});
it('should fetch users', () => {
const mockUsers: User[] = [
{ id: '1', firstName: 'John', lastName: 'Doe', email: 'john@example.com', postsCount: 5, followersCount: 10 }
];
service.getUsers().subscribe(users => {
expect(users).toEqual(mockUsers);
});
const req = httpMock.expectOne(`${service['apiUrl']}`);
expect(req.request.method).toBe('GET');
req.flush({ data: mockUsers, success: true, message: 'Success' });
});
it('should create a user', () => {
const newUser: User = { id: '2', firstName: 'Jane', lastName: 'Smith', email: 'jane@example.com', postsCount: 0, followersCount: 0 };
const createRequest = { firstName: 'Jane', lastName: 'Smith', email: 'jane@example.com' };
service.createUser(createRequest).subscribe(user => {
expect(user).toEqual(newUser);
});
const req = httpMock.expectOne(`${service['apiUrl']}`);
expect(req.request.method).toBe('POST');
expect(req.request.body).toEqual(createRequest);
req.flush({ data: newUser, success: true, message: 'User created' });
});
});
E2E Testing with Cypress
// Cypress E2E test
describe('User Management', () => {
beforeEach(() => {
cy.visit('/users');
cy.login('admin@example.com', 'password');
});
it('should display list of users', () => {
cy.get('[data-cy=user-list]').should('be.visible');
cy.get('[data-cy=user-card]').should('have.length.greaterThan', 0);
});
it('should create a new user', () => {
cy.get('[data-cy=add-user-button]').click();
cy.get('[data-cy=firstName-input]').type('John');
cy.get('[data-cy=lastName-input]').type('Doe');
cy.get('[data-cy=email-input]').type('john.doe@example.com');
cy.get('[data-cy=bio-textarea]').type('Software developer');
cy.get('[data-cy=submit-button]').click();
cy.get('[data-cy=success-message]').should('contain', 'User created successfully');
cy.get('[data-cy=user-list]').should('contain', 'John Doe');
});
it('should edit an existing user', () => {
cy.get('[data-cy=user-card]').first().find('[data-cy=edit-button]').click();
cy.get('[data-cy=firstName-input]').clear().type('Jane');
cy.get('[data-cy=submit-button]').click();
cy.get('[data-cy=success-message]').should('contain', 'User updated successfully');
});
it('should delete a user', () => {
cy.get('[data-cy=user-card]').first().find('[data-cy=delete-button]').click();
cy.get('[data-cy=confirm-delete]').click();
cy.get('[data-cy=success-message]').should('contain', 'User deleted successfully');
});
});
Deployment
Docker Configuration
# Multi-stage build for Angular application
FROM node:18-alpine AS build
WORKDIR /app
# Copy package files
COPY package*.json ./
RUN npm ci --only=production
# Copy source code
COPY . .
# Build the application
RUN npm run build --prod
# Production stage
FROM nginx:alpine
# Copy custom nginx configuration
COPY nginx.conf /etc/nginx/nginx.conf
# Copy built application
COPY --from=build /app/dist/angular-app /usr/share/nginx/html
# Expose port
EXPOSE 80
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD curl -f http://localhost/ || exit 1
CMD ["nginx", "-g", "daemon off;"]
Kubernetes Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: angular-app
labels:
app: angular-app
spec:
replicas: 3
selector:
matchLabels:
app: angular-app
template:
metadata:
labels:
app: angular-app
spec:
containers:
- name: angular-app
image: your-registry/angular-app:latest
ports:
- containerPort: 80
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 512Mi
livenessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 5
periodSeconds: 5
---
apiVersion: v1
kind: Service
metadata:
name: angular-app-service
spec:
selector:
app: angular-app
ports:
- port: 80
targetPort: 80
type: ClusterIP
Quick Start
-
Generate the application:
archetect render git@github.com:p6m-archetypes/typescript-angular-basic.archetype.git -
Install dependencies:
npm install -
Start development server:
ng serve -
Access the application:
http://localhost:4200 -
Run tests:
# Unit tests
npm run test
# E2E tests
npm run e2e
# Test coverage
npm run test:coverage -
Build for production:
ng build --prod
Best Practices
Component Design
- Use standalone components for better tree-shaking
- Implement OnPush change detection for performance
- Follow single responsibility principle
- Use reactive patterns with RxJS
State Management
- Use services for simple state management
- Implement NgRx for complex applications
- Use signals for reactive programming
- Avoid direct DOM manipulation
Performance
- Implement lazy loading for routes
- Use trackBy functions in *ngFor
- Optimize bundle size with tree-shaking
- Implement proper caching strategies
Security
- Sanitize user inputs
- Use HTTPS in production
- Implement Content Security Policy
- Validate data on both client and server