HTTP Patterns in Practice
CRUD Service with Full Support
1@Injectable({ providedIn: 'root' })
2export class ApiService<T extends { id: number }> {
3 constructor(
4 private http: HttpClient,
5 private baseUrl: string
6 ) {}
7
8 getAll(params?: Record<string, string>): Observable<T[]> {
9 return this.http.get<T[]>(this.baseUrl, {
10 params: params ? new HttpParams({ fromObject: params }) : undefined
11 });
12 }
13
14 getById(id: number): Observable<T> {
15 return this.http.get<T>(\`\${this.baseUrl}/\${id}\`);
16 }
17
18 create(item: Omit<T, 'id'>): Observable<T> {
19 return this.http.post<T>(this.baseUrl, item);
20 }
21
22 update(item: T): Observable<T> {
23 return this.http.put<T>(\`\${this.baseUrl}/\${item.id}\`, item);
24 }
25
26 delete(id: number): Observable<void> {
27 return this.http.delete<void>(\`\${this.baseUrl}/\${id}\`);
28 }
29}
30
31// Usage
32@Injectable({ providedIn: 'root' })
33export class SamuraiApiService extends ApiService<Samurai> {
34 constructor(http: HttpClient) {
35 super(http, '/api/samurai');
36 }
37}
HTTP with Signals (Angular 17+)
1@Component({
2 template: \`
3 @if (loading()) {
4 <app-spinner />
5 } @else if (error()) {
6 <div class="error">{{ error() }}</div>
7 } @else {
8 @for (samurai of samuraiList(); track samurai.id) {
9 <app-samurai-card [samurai]="samurai" />
10 }
11 }
12 \`
13})
14export class SamuraiListComponent {
15 private samuraiService = inject(SamuraiService);
16
17 // State as signals
18 samuraiList = signal<Samurai[]>([]);
19 loading = signal(true);
20 error = signal<string | null>(null);
21
22 constructor() {
23 this.loadSamurai();
24 }
25
26 loadSamurai(): void {
27 this.loading.set(true);
28 this.error.set(null);
29
30 this.samuraiService.getAll().pipe(
31 takeUntilDestroyed()
32 ).subscribe({
33 next: (samurai) => {
34 this.samuraiList.set(samurai);
35 this.loading.set(false);
36 },
37 error: (err) => {
38 this.error.set(err.message);
39 this.loading.set(false);
40 }
41 });
42 }
43}
Resource API (Angular 19)
1import { resource } from '@angular/core';
2
3@Component({
4 template: \`
5 @switch (samuraiResource.status()) {
6 @case ('loading') {
7 <app-spinner />
8 }
9 @case ('error') {
10 <div class="error">{{ samuraiResource.error() }}</div>
11 }
12 @case ('success') {
13 <app-samurai-detail [samurai]="samuraiResource.value()!" />
14 }
15 }
16 \`
17})
18export class SamuraiDetailComponent {
19 id = input.required<number>();
20
21 samuraiResource = resource({
22 request: () => this.id(),
23 loader: async ({ request: id }) => {
24 const response = await fetch(\`/api/samurai/\${id}\`);
25 if (!response.ok) throw new Error('Failed to load');
26 return response.json() as Promise<Samurai>;
27 }
28 });
29
30 refresh(): void {
31 this.samuraiResource.reload();
32 }
33}