ASP.NET Core Web API · Quality & Delivery

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.

Advanced 8 min Lesson 37 of 37Examples target .NET 9.
0/37 in ASP.NET Core Web API
On this page · 8 min · Advanced

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#

TypeScript Illustrative
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#

TypeScriptsrc/app/products.component.ts Illustrative
// 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] });
        }
      },
    });
  }
}
Expected output
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#

  1. AuthStore keeps the token in a signal; the functional interceptor adds it to every request and handles 401 centrally.
  2. ProductService wraps HttpClient with typed methods and reads the API URL from the environment file.
  3. The component keeps products, loading and error state in signals and renders them with @if and @for (track p.id).
  4. On a 400, problem.errors keys (Name, Price) are mapped to camelCase form controls and set as server errors.
  5. 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#

Adding headers in every service call

Copying the Authorization header everywhere is error-prone. Use an interceptor.

Not unsubscribing from long-lived observables

HttpClient calls complete on their own, but other streams need takeUntilDestroyed or the async pipe.

Ignoring server validation

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?
this.http.delete<void>(`${this.url}/${id}`) and this.products.update(list => list.filter(p => p.id !== id)).
Show solution
TypeScript Illustrative
// 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#

Angular + ASP.NET Core API Project quizMultiple choice Question 1 of 3

Where is the bearer token added to requests in this project?

Score so far: 0/0

Want more? Practise all ASP.NET Core Web API MCQs · ASP.NET Core Web API interview questions

My notes#

Saved in this browser.

Progress is saved in this browser. Sign in to sync.