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.
On this page · 6 min · Advanced
On this page
- 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
- 6 min
- Difficulty
- Advanced
- Updated
- Oct 28, 2026
Related tutorials
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#
{
"/api": {
"target": "https://localhost:7001",
"secure": false,
"changeOrigin": true
}
}
Code example: A product form that shows ASP.NET Core validation errors#
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.');
}
},
});
}
}
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#
- The TypeScript types mirror the C# records in camelCase.
- In development, /api/products is proxied to Kestrel by proxy.conf.json.
- On 400, the ProblemDetails errors keys (“Name”, “Price”) are mapped to the form controls.
- setErrors({ server: … }) attaches the server message; the template shows it under the field.
- 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.conf.json does nothing unless angular.json’s serve options (or ng serve --proxy-config) point to it.
Expecting product.Name when the API sends name gives undefined. Mirror camelCase.
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?
Show solution
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#
What does proxy.conf.json do during ng serve?
Want more? Practise all Angular MCQs · Angular interview questions
My notes#
Saved in this browser.