Skip to main content

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

  1. Generate the application:

    archetect render git@github.com:p6m-archetypes/typescript-angular-basic.archetype.git
  2. Install dependencies:

    npm install
  3. Start development server:

    ng serve
  4. Access the application:

    http://localhost:4200
  5. Run tests:

    # Unit tests
    npm run test

    # E2E tests
    npm run e2e

    # Test coverage
    npm run test:coverage
  6. 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