Angular · Production

Angular with ASP.NET Core API

Angular and ASP.NET Core are a classic enterprise pairing — both strongly typed, both opinionated. A few conventions make them work together smoothly from day one.

Advanced 6 min Lesson 37 of 38Examples target Angular 20 with standalone components. Each lesson states the version it was verified against.
Before you start:REST API Integration
0/38 in Angular
On this page · 6 min · Advanced

Learning objectives#

  • Proxy /api to Kestrel during development
  • Configure CORS for production
  • Mirror C# DTOs as TypeScript types
  • Show ProblemDetails validation errors on the right form fields

Explanation#

Development: create proxy.conf.json mapping "/api" to "https://localhost:7001" with secure: false, and reference it in angular.json (serve → proxyConfig). The browser sees one origin, so no CORS setup is needed while developing.

Production: serve both from one domain (reverse proxy, or the API serving the Angular build from wwwroot), or add a CORS policy in Program.cs for the exact Angular origin.

ASP.NET Core returns camelCase JSON. Mirror each DTO as a TypeScript type (or generate them from the OpenAPI document with tools like NSwag or openapi-typescript).

[ApiController] returns 400 ValidationProblemDetails: { title, status, errors: { "Name": ["..."] } }. Map those keys to form controls with setErrors so messages appear next to fields.

For JWT auth, an interceptor adds Authorization: Bearer <token>; the API uses AddAuthentication().AddJwtBearer(...) and [Authorize].

Syntax#

JSON Illustrative
{
  "/api": {
    "target": "https://localhost:7001",
    "secure": false,
    "changeOrigin": true
  }
}

Code example: A product form that shows ASP.NET Core validation errors#

TypeScriptproduct-form.ts Illustrative
import { Component, Injectable, inject, signal } from '@angular/core';
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
import { FormBuilder, ReactiveFormsModule } from '@angular/forms';


// Mirrors the C# DTOs (ASP.NET Core serialises them in camelCase)
//   public record CreateProductRequest(string Name, decimal Price);
//   public record ProductDto(int Id, string Name, decimal Price);
export type CreateProductRequest = { name: string; price: number };
export type ProductDto = { id: number; name: string; price: number };


type ValidationProblem = { title: string; status: number; errors?: Record<string, string[]> };


@Injectable({ providedIn: 'root' })
export class ProductApi {
  private http = inject(HttpClient);
  create(body: CreateProductRequest) {
    return this.http.post<ProductDto>('/api/products', body);   // proxied to Kestrel in dev
  }
}


@Component({
  selector: 'app-product-form',
  imports: [ReactiveFormsModule],
  template: `
    <form [formGroup]="form" (ngSubmit)="save()">
      <label>Name <input formControlName="name" /></label>
      @if (form.controls.name.errors?.['server']) { <small>{{ form.controls.name.errors?.['server'] }}</small> }


      <label>Price <input formControlName="price" type="number" /></label>
      @if (form.controls.price.errors?.['server']) { <small>{{ form.controls.price.errors?.['server'] }}</small> }


      <button [disabled]="saving()">Save</button>
      @if (message()) { <p>{{ message() }}</p> }
    </form>
  `,
})
export class ProductForm {
  private api = inject(ProductApi);
  form = inject(FormBuilder).nonNullable.group({ name: '', price: 0 });
  saving = signal(false);
  message = signal('');


  save() {
    this.saving.set(true);
    this.message.set('');
    this.api.create(this.form.getRawValue()).subscribe({
      next: product => { this.saving.set(false); this.message.set(`Saved #${product.id}: ${product.name}`); },
      error: (err: HttpErrorResponse) => {
        this.saving.set(false);
        const problem = err.error as ValidationProblem | null;
        if (err.status === 400 && problem?.errors) {
          for (const [key, messages] of Object.entries(problem.errors)) {
            const control = this.form.get(key.charAt(0).toLowerCase() + key.slice(1));   // "Name" → "name"
            control?.setErrors({ server: messages[0] });
            control?.markAsTouched();
          }
          this.message.set(problem.title);
        } else {
          this.message.set('Could not save the product. Please try again.');
        }
      },
    });
  }
}
Expected output
Submitting an empty form sends { "name": "", "price": 0 } to the ASP.NET Core API. [ApiController] answers 400 with ProblemDetails, and the form shows “The Name field is required.” under Name, “The field Price must be between 1 and 100000.” under Price, and the title “One or more validation errors occurred.”. A valid submission shows “Saved #12: Notebook”.

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. The TypeScript types mirror the C# records in camelCase.
  2. In development, /api/products is proxied to Kestrel by proxy.conf.json.
  3. On 400, the ProblemDetails errors keys (“Name”, “Price”) are mapped to the form controls.
  4. setErrors({ server: … }) attaches the server message; the template shows it under the field.
  5. Other failures show a generic message.

Real-world use case#

A government permits portal uses Angular + ASP.NET Core: validation rules live in C# Data Annotations and FluentValidation, and the Angular forms display whatever the API reports — no duplicated rule lists drifting apart.

Common mistakes#

Proxy file not referenced

proxy.conf.json does nothing unless angular.json’s serve options (or ng serve --proxy-config) point to it.

PascalCase models

Expecting product.Name when the API sends name gives undefined. Mirror camelCase.

CORS order in Program.cs

app.UseCors() must run before the endpoints (MapControllers), or preflights fail.

Best practices#

  • Proxy in dev, same origin or strict CORS in prod
  • Generate TS types from OpenAPI
  • Treat ProblemDetails as the error contract
  • JWT via an interceptor

Remember this#

Dev: proxy.conf.json → Kestrel.

JSON is camelCase on both sides.

400 + errors → setErrors on matching controls.

Practice exercise#

Your task: Write the Program.cs CORS policy for an Angular app served at https://portal.example.com and http://localhost:4200.

Need a hint?
AddCors with WithOrigins(...).AllowAnyHeader().AllowAnyMethod(); UseCors before MapControllers.
Show solution
C# Illustrative
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddCors(options => options.AddPolicy("angular", policy => policy
    .WithOrigins("https://portal.example.com", "http://localhost:4200")
    .AllowAnyHeader()
    .AllowAnyMethod()));


var app = builder.Build();
app.UseCors("angular");
app.MapControllers();
app.Run();

Mini quiz#

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

What does proxy.conf.json do during ng serve?

Score so far: 0/0

Want more? Practise all Angular MCQs · Angular interview questions

My notes#

Saved in this browser.

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