Angular + ASP.NET Core API Project
ShopSmart’s admin team prefers Angular. In this project an Angular 20 app talks to the same Web API: a ProductService built on HttpClient, an interceptor that adds the bearer token and handles 401, and a reactive form that displays the API’s validation errors.
On this page · 8 min · Advanced
On this page
- In simple words
- Learning objectives
- Explanation
- Syntax
- Code example
- Step by step
- Real-world use
- Common mistakes
- Best practices
- Remember this
- Practice exercise
- Mini quiz
- Related topics
- My notes
- Reading time
- 8 min
- Difficulty
- Advanced
- Updated
- Oct 29, 2026
Related tutorials
In simple words#
The Angular + API project pairs a structured shop front (Angular) with the same reliable back office (ASP.NET Core API).
- HttpClient (Angular)
- Calls the API.
- Services + DI
- Same idea on both sides.
- Interceptors
- Attach the JWT to every request.
Learning objectives#
- Call the API with a typed HttpClient service
- Add the JWT and handle 401 with a functional interceptor
- Store state with signals
- Map problem-details errors onto reactive form controls
Explanation#
API side: CORS for http://localhost:4200 (ng serve) and the production origin; the same DTOs and problem details as the React project.
provideHttpClient(withInterceptors([authInterceptor])) in app.config.ts registers HttpClient and a functional interceptor that clones each request with Authorization: Bearer <token> and redirects to /login on 401.
ProductService (providedIn: root) exposes typed methods: list(), create(). Components inject it with inject(ProductService) and keep data in signals.
Reactive forms (FormBuilder, Validators) give instant client feedback; the server stays the source of truth. On HttpErrorResponse with status 400, copy problem.errors onto the matching controls with setErrors({ server: message }).
Use @if / @for blocks in templates (with track) and show loading and error states.
Syntax#
export const authInterceptor: HttpInterceptorFn = (req, next) => {
const token = inject(AuthStore).token();
return next(token ? req.clone({ setHeaders: { Authorization: `Bearer ${token}` } }) : req);
};
Code example: Service, interceptor and a products component#
// src/app/auth.ts
import { HttpErrorResponse, type HttpInterceptorFn } from '@angular/common/http';
import { inject, Injectable, signal } from '@angular/core';
import { Router } from '@angular/router';
import { catchError, throwError } from 'rxjs';
@Injectable({ providedIn: 'root' })
export class AuthStore {
readonly token = signal<string | null>(null);
}
export const authInterceptor: HttpInterceptorFn = (req, next) => {
const auth = inject(AuthStore);
const router = inject(Router);
const token = auth.token();
const authReq = token ? req.clone({ setHeaders: { Authorization: `Bearer ${token}` } }) : req;
return next(authReq).pipe(
catchError((err: unknown) => {
if (err instanceof HttpErrorResponse && err.status === 401) {
auth.token.set(null);
void router.navigate(['/login']);
}
return throwError(() => err);
}),
);
};
// src/app/product.service.ts
import { HttpClient } from '@angular/common/http';
import { inject, Injectable } from '@angular/core';
import { environment } from '../environments/environment';
export interface Product { id: number; name: string; price: number }
@Injectable({ providedIn: 'root' })
export class ProductService {
private readonly http = inject(HttpClient);
private readonly url = `${environment.apiUrl}/api/products`;
list() { return this.http.get<Product[]>(this.url); }
create(input: { name: string; price: number }) { return this.http.post<Product>(this.url, input); }
}
// src/app/products.component.ts
import { HttpErrorResponse } from '@angular/common/http';
import { Component, inject, OnInit, signal } from '@angular/core';
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
import { ProductService, type Product } from './product.service';
@Component({
selector: 'app-products',
imports: [ReactiveFormsModule],
template: `
<h1>Products</h1>
@if (loading()) { <p>Loading products…</p> }
@if (loadError()) { <p role="alert">{{ loadError() }}</p> }
<ul>
@for (p of products(); track p.id) { <li>{{ p.name }} — ₹{{ p.price }}</li> }
</ul>
<form [formGroup]="form" (ngSubmit)="save()">
<label>Name <input formControlName="name" /></label>
@if (form.controls.name.errors?.['server']) { <small class="error">{{ form.controls.name.errors?.['server'] }}</small> }
<label>Price <input formControlName="price" type="number" /></label>
@if (form.controls.price.errors?.['server']) { <small class="error">{{ form.controls.price.errors?.['server'] }}</small> }
<button [disabled]="saving()">{{ saving() ? 'Saving…' : 'Add product' }}</button>
</form>
`,
})
export class ProductsComponent implements OnInit {
private readonly productsApi = inject(ProductService);
private readonly fb = inject(FormBuilder);
readonly products = signal<Product[]>([]);
readonly loading = signal(true);
readonly loadError = signal('');
readonly saving = signal(false);
readonly form = this.fb.nonNullable.group({
name: ['', Validators.required],
price: [0, [Validators.required, Validators.min(0.01)]],
});
ngOnInit() {
this.productsApi.list().subscribe({
next: list => { this.products.set(list); this.loading.set(false); },
error: () => { this.loadError.set('Could not load products.'); this.loading.set(false); },
});
}
save() {
this.saving.set(true);
this.productsApi.create(this.form.getRawValue()).subscribe({
next: created => {
this.products.update(list => [...list, created]);
this.form.reset();
this.saving.set(false);
},
error: (err: HttpErrorResponse) => {
this.saving.set(false);
if (err.status !== 400) return;
const errors: Record<string, string[]> = err.error?.errors ?? {};
for (const [field, messages] of Object.entries(errors)) {
const control = this.form.get(field.charAt(0).toLowerCase() + field.slice(1)); // "Name" → "name"
control?.setErrors({ server: messages[0] });
}
},
});
}
}
With app.config.ts providing provideHttpClient(withInterceptors([authInterceptor])), the component lists products from the API. Saving an empty name shows “The Name field is required.” under the Name input — the message comes from the API’s problem details. A 401 clears the token and navigates to /login.
This example is illustrative. Copy or download it and run it in your own environment; the expected output is shown above.
Step-by-step explanation#
- AuthStore keeps the token in a signal; the functional interceptor adds it to every request and handles 401 centrally.
- ProductService wraps HttpClient with typed methods and reads the API URL from the environment file.
- The component keeps products, loading and error state in signals and renders them with @if and @for (track p.id).
- On a 400, problem.errors keys (Name, Price) are mapped to camelCase form controls and set as server errors.
- Client validators give instant feedback, but the server’s messages are what the user finally sees.
Real-world use case#
ShopSmart’s Angular admin app uses this service + interceptor pattern for 25 resources. Adding a new screen means a new service and component; authentication, error handling and validation display already work.
Common mistakes#
Copying the Authorization header everywhere is error-prone. Use an interceptor.
HttpClient calls complete on their own, but other streams need takeUntilDestroyed or the async pipe.
Only client validators means rules like “SKU already exists” never reach the user. Map problem.errors onto controls.
Best practices#
- provideHttpClient + functional interceptors
- Typed services per resource
- Signals for component state
- Server errors mapped to form controls
Remember this#
Interceptor: add Bearer token, handle 401 once.
Service per resource, inject() in components.
400 → errors → control.setErrors({ server }).
Practice exercise#
Your task: Add a delete(id) method to ProductService and a Delete button in the list that removes the product from the products signal after success.
Need a hint?
Show solution
// product.service.ts (addition)
delete(id: number) { return this.http.delete<void>(`${this.url}/${id}`); }
// products.component.ts (addition)
remove(id: number) {
this.productsApi.delete(id).subscribe(() => this.products.update(list => list.filter(p => p.id !== id)));
}
// template (inside the @for)
// <li>{{ p.name }} — ₹{{ p.price }} <button (click)="remove(p.id)">Delete</button></li>
Mini quiz#
Where is the bearer token added to requests in this project?
Want more? Practise all ASP.NET Core Web API MCQs · ASP.NET Core Web API interview questions
My notes#
Saved in this browser.