๐ Building a Full-Stack App with Angular + .NET Core Web API: The Complete Beginner's Guide
From two empty folders to a fully connected Angular frontend and .NET Core backend โ step by step, with CORS explained, HttpClient wired up, and both servers running locally in under 30 minutes
Hi ๐, I'm Tushar Patil. Currently I am working as Frontend Developer (Angular) and also have expertise with .Net Core and Framework.
๐ค Why Angular + .NET Core?
Angular and ASP.NET Core together give you a rock-solid full-stack foundation for 2026 and beyond. Both are strongly typed, both are built with scalability in mind, and they share a TypeScript/.NET C# boundary that makes data contracts crystal clear.
If you already know Angular, .NET Core Web API will feel surprisingly familiar:
- Dependency Injection โ โ both have it built in
- Typed models โ โ C# records map cleanly to TypeScript interfaces
- First-class tooling โ
โ
dotnetCLI mirrors Angular CLI - Excellent SSR story โ โ Angular SSR + .NET hosting work seamlessly together
By the end of this post, you'll have:
- ๐ฅ๏ธ A .NET 9 Web API serving a
Productsendpoint - ๐ An Angular 21 app calling that API using
HttpClient - ๐ CORS properly configured so they talk without browser errors
- โก Both servers running locally with one command each
Let's build it. ๐ ๏ธ
๐งฐ Prerequisites
Make sure you have these installed before we start:
# Check .NET version โ need 9.0+
dotnet --version # should output 9.x.x
# Check Node version โ need 18.19+
node --version # should output 18.x or higher
# Check Angular CLI version โ need 17+
ng version # should output Angular CLI: 17.x or higher
๐ฆ Install links if needed:
- .NET 9 SDK โ dotnet.microsoft.com/download
- Node.js โ nodejs.org
- Angular CLI โ
npm install -g @angular/cli
๐ก You'll also need a code editor. VS Code works perfectly for both โ install the C# Dev Kit extension for .NET IntelliSense.
๐๏ธ Project structure
We'll keep things clean with a monorepo structure โ both projects in the same parent folder:
fullstack-app/
โโโ backend/ โ .NET Core Web API
โโโ frontend/ โ Angular app
Let's create the parent folder first:
mkdir fullstack-app && cd fullstack-app
๐ฅ๏ธ Part 1: Building the .NET Core Web API
Step 1: Create the Web API project
dotnet new webapi -n backend --use-controllers
cd backend
The --use-controllers flag scaffolds a classic controller-based API (as opposed to minimal APIs). This gives us a clean pattern for organising endpoints as our app grows.
Your project structure will look like this:
backend/
Controllers/
WeatherForecastController.cs โ default example controller
Properties/
launchSettings.json
appsettings.json
appsettings.Development.json
Program.cs โ app entry point
backend.csproj
Step 2: Create the Products model
Let's build something more realistic than weather data. We'll create a Product API. First, create a Models folder:
mkdir Models
Create Models/Product.cs:
// Models/Product.cs
namespace backend.Models;
public class Product
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
public decimal Price { get; set; }
public string Category { get; set; } = string.Empty;
public bool InStock { get; set; }
}
๐ก Notice the
namespace backend.Modelssyntax โ this is a file-scoped namespace, introduced in C# 10. It's cleaner than wrapping everything in curly braces.
Step 3: Create the Products controller
Create Controllers/ProductsController.cs:
// Controllers/ProductsController.cs
using Microsoft.AspNetCore.Mvc;
using backend.Models;
namespace backend.Controllers;
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
// ๐๏ธ In-memory data for this tutorial
// In a real app, you'd inject a DbContext or repository here
private static readonly List<Product> _products = new()
{
new Product { Id = 1, Name = "Angular Course", Description = "Learn Angular from scratch", Price = 49.99m, Category = "Education", InStock = true },
new Product { Id = 2, Name = "VS Code Extension", Description = "Boost your productivity", Price = 0m, Category = "Tools", InStock = true },
new Product { Id = 3, Name = "TypeScript Handbook", Description = "Master TypeScript in 2026", Price = 29.99m, Category = "Books", InStock = true },
new Product { Id = 4, Name = "ReSharper License", Description = "Refactoring tools for .NET", Price = 179.00m, Category = "Tools", InStock = false },
new Product { Id = 5, Name = "Clean Code Book", Description = "Write better code every day", Price = 34.99m, Category = "Books", InStock = true },
};
// โ
GET api/products โ returns all products
[HttpGet]
public ActionResult<IEnumerable<Product>> GetAll()
{
return Ok(_products);
}
// โ
GET api/products/1 โ returns a single product
[HttpGet("{id:int}")]
public ActionResult<Product> GetById(int id)
{
var product = _products.FirstOrDefault(p => p.Id == id);
if (product is null)
return NotFound(new { Message = $"Product {id} not found" });
return Ok(product);
}
// โ
POST api/products โ creates a new product
[HttpPost]
public ActionResult<Product> Create([FromBody] Product product)
{
product.Id = _products.Max(p => p.Id) + 1;
_products.Add(product);
return CreatedAtAction(nameof(GetById), new { id = product.Id }, product);
}
// โ
DELETE api/products/1 โ removes a product
[HttpDelete("{id:int}")]
public IActionResult Delete(int id)
{
var product = _products.FirstOrDefault(p => p.Id == id);
if (product is null)
return NotFound();
_products.Remove(product);
return NoContent();
}
}
Step 4: Configure CORS ๐
This is the most important step for making Angular and .NET talk to each other locally. By default, your Angular app runs on http://localhost:4200 and your API on http://localhost:5000. These are different origins โ and the browser will block cross-origin requests unless the API explicitly allows them.
Open Program.cs and replace its contents with this:
// Program.cs
var builder = WebApplication.CreateBuilder(args);
// โ
Step 1: Register CORS policy
builder.Services.AddCors(options =>
{
options.AddPolicy("AllowAngularApp", policy =>
{
policy
.WithOrigins("http://localhost:4200") // ๐ Angular dev server
.AllowAnyHeader()
.AllowAnyMethod();
});
});
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
// โ
Step 2: Apply CORS middleware
// โ ๏ธ Order matters: UseCors MUST come before UseAuthorization
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseCors("AllowAngularApp"); // ๐ BEFORE UseAuthorization
app.UseAuthorization();
app.MapControllers();
app.Run();
โ ๏ธ Order matters!
app.UseCors()must be called beforeapp.UseAuthentication()andapp.UseAuthorization()inProgram.cs. Getting this wrong is the most common reason CORS silently fails.
Step 5: Configure the API URL in appsettings
Update appsettings.Development.json to store the allowed Angular origins โ this makes it easy to change without touching code:
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedOrigins": [
"http://localhost:4200"
]
}
And update Program.cs to read from config instead of hard-coding:
// Read origins from config
var allowedOrigins = builder.Configuration
.GetSection("AllowedOrigins")
.Get<string[]>() ?? Array.Empty<string>();
builder.Services.AddCors(options =>
{
options.AddPolicy("AllowAngularApp", policy =>
{
policy
.WithOrigins(allowedOrigins) // ๐ from appsettings
.AllowAnyHeader()
.AllowAnyMethod();
});
});
Step 6: Run the API ๐
# From the backend/ folder
dotnet run
You'll see output like:
info: Microsoft.Hosting.Lifetime[14]
Now listening on: http://localhost:5000
Open http://localhost:5000/api/products in your browser โ you should see your JSON array of products. ๐
Also check the Swagger UI at http://localhost:5000/swagger โ a beautiful interactive API explorer, free with every .NET project.
๐ Part 2: Building the Angular Frontend
Step 7: Create the Angular project
Back in the fullstack-app folder:
cd .. # back to fullstack-app/
ng new frontend --standalone --style=css --routing=true
cd frontend
Step 8: Set up environment files
Angular's environment system lets you configure different API URLs for development and production without changing any code.
Create src/environments/environment.ts:
// src/environments/environment.ts (production)
export const environment = {
production: true,
apiUrl: 'https://your-api.azurewebsites.net/api', // update for prod
};
Create src/environments/environment.development.ts:
// src/environments/environment.development.ts (local dev)
export const environment = {
production: false,
apiUrl: 'http://localhost:5000/api', // ๐ .NET API running locally
};
๐ก Angular CLI automatically uses
environment.development.tsduringng serveandenvironment.tsduringng build. You never need to switch manually.
Step 9: Configure HttpClient in app.config.ts
Open src/app/app.config.ts and add provideHttpClient():
// src/app/app.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideHttpClient, withFetch } from '@angular/common/http';
import { routes } from './app.routes';
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(routes),
provideHttpClient(withFetch()), // ๐ enables HttpClient across the whole app
],
};
๐ก
withFetch()makes Angular use the native browserfetch()API under the hood instead ofXMLHttpRequest. This is the modern approach and pairs well with Angular's SSR story.
Step 10: Create the Product model
Mirror the C# model in TypeScript. Create src/app/models/product.model.ts:
// src/app/models/product.model.ts
export interface Product {
id: number;
name: string;
description: string;
price: number;
category: string;
inStock: boolean;
}
๐ก Notice how the TypeScript interface maps directly to the C# model. This is one of the best parts of the Angular + .NET stack โ your models are mirrors of each other, and the JSON serialiser handles the camelCase conversion automatically.
Step 11: Create the Products service
Services encapsulate all HTTP logic. Create src/app/services/product.service.ts:
// src/app/services/product.service.ts
import { Injectable, inject } from '@angular/core';
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
import { Observable, catchError, throwError } from 'rxjs';
import { environment } from '../../environments/environment';
import { Product } from '../models/product.model';
@Injectable({ providedIn: 'root' })
export class ProductService {
private http = inject(HttpClient);
private apiUrl = `${environment.apiUrl}/products`;
// ๐ฆ Get all products
getAll(): Observable<Product[]> {
return this.http.get<Product[]>(this.apiUrl).pipe(
catchError(this.handleError)
);
}
// ๐ Get a single product by ID
getById(id: number): Observable<Product> {
return this.http.get<Product>(`\({this.apiUrl}/\){id}`).pipe(
catchError(this.handleError)
);
}
// โ Create a new product
create(product: Omit<Product, 'id'>): Observable<Product> {
return this.http.post<Product>(this.apiUrl, product).pipe(
catchError(this.handleError)
);
}
// ๐๏ธ Delete a product
delete(id: number): Observable<void> {
return this.http.delete<void>(`\({this.apiUrl}/\){id}`).pipe(
catchError(this.handleError)
);
}
// ๐จ Centralised error handler
private handleError(error: HttpErrorResponse): Observable<never> {
let message = 'An unexpected error occurred';
if (error.status === 0) {
message = 'Cannot reach the server. Is the .NET API running?';
} else if (error.status === 404) {
message = 'Resource not found';
} else if (error.error?.message) {
message = error.error.message;
}
console.error('API Error:', error);
return throwError(() => new Error(message));
}
}
Step 12: Build the Products component
Now let's build a component that fetches and displays the products. Generate it:
ng generate component products
Update src/app/products/products.component.ts:
// src/app/products/products.component.ts
import { Component, inject, signal, computed, OnInit } from '@angular/core';
import { CurrencyPipe, NgClass } from '@angular/common';
import { ProductService } from '../services/product.service';
import { Product } from '../models/product.model';
@Component({
selector: 'app-products',
standalone: true,
imports: [CurrencyPipe, NgClass],
templateUrl: './products.component.html',
styleUrl: './products.component.css',
})
export class ProductsComponent implements OnInit {
private productService = inject(ProductService);
// ๐ฆ State signals
products = signal<Product[]>([]);
loading = signal(true);
error = signal<string | null>(null);
// ๐งฎ Derived signals
inStockCount = computed(() => this.products().filter(p => p.inStock).length);
outOfStockCount = computed(() => this.products().filter(p => !p.inStock).length);
totalValue = computed(() =>
this.products().reduce((sum, p) => sum + p.price, 0)
);
ngOnInit() {
this.loadProducts();
}
loadProducts() {
this.loading.set(true);
this.error.set(null);
this.productService.getAll().subscribe({
next: (data) => { this.products.set(data); this.loading.set(false); },
error: (err) => { this.error.set(err.message); this.loading.set(false); },
});
}
deleteProduct(id: number) {
this.productService.delete(id).subscribe({
next: () => this.products.update(items => items.filter(p => p.id !== id)),
});
}
}
Update src/app/products/products.component.html:
<!-- products.component.html -->
<div class="products-page">
<h1>๐๏ธ Products</h1>
<!-- Summary stats -->
<div class="stats">
<span>โ
In stock: {{ inStockCount() }}</span>
<span>โ Out of stock: {{ outOfStockCount() }}</span>
<span>๐ฐ Total value: {{ totalValue() | currency }}</span>
</div>
<!-- Loading state -->
@if (loading()) {
<p class="loading">โณ Loading products from API...</p>
}
<!-- Error state -->
@if (error()) {
<div class="error">
<p>โ {{ error() }}</p>
<button (click)="loadProducts()">๐ Retry</button>
</div>
}
<!-- Products grid -->
@if (!loading() && !error()) {
<div class="product-grid">
@for (product of products(); track product.id) {
<div class="product-card" [ngClass]="{ 'out-of-stock': !product.inStock }">
<div class="product-header">
<h3>{{ product.name }}</h3>
<span class="badge">{{ product.category }}</span>
</div>
<p>{{ product.description }}</p>
<div class="product-footer">
<strong>{{ product.price | currency }}</strong>
<span [ngClass]="product.inStock ? 'in-stock' : 'oos'">
{{ product.inStock ? 'โ
In Stock' : 'โ Out of Stock' }}
</span>
</div>
<button (click)="deleteProduct(product.id)">๐๏ธ Delete</button>
</div>
}
</div>
}
</div>
Step 13: Hook it into the router
Update src/app/app.routes.ts:
// src/app/app.routes.ts
import { Routes } from '@angular/router';
export const routes: Routes = [
{
path: '',
redirectTo: 'products',
pathMatch: 'full',
},
{
path: 'products',
loadComponent: () =>
import('./products/products.component').then(m => m.ProductsComponent),
},
];
Update src/app/app.component.html to just the router outlet:
<!-- app.component.html -->
<router-outlet />
๐ Part 3: Angular dev proxy โ the elegant alternative to CORS
CORS is the right solution for production. But for local development, there's an even cleaner approach: the Angular dev server proxy. Instead of the browser making a cross-origin request to http://localhost:5000, it makes a same-origin request to http://localhost:4200/api โ and Angular's dev server silently forwards it to the .NET API. No CORS headers needed. ๐ง
Setting up the proxy
Create proxy.conf.json at the root of your frontend/ folder:
{
"/api": {
"target": "http://localhost:5000",
"secure": false,
"changeOrigin": true,
"logLevel": "info"
}
}
Register it in angular.json under serve > options:
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"options": {
"proxyConfig": "proxy.conf.json"
}
}
Now update environment.development.ts to use a relative URL:
// environment.development.ts โ with proxy
export const environment = {
production: false,
apiUrl: '/api', // ๐ relative โ proxy handles the routing
};
With the proxy in place, http.get('/api/products') from Angular gets forwarded to http://localhost:5000/api/products โ and the browser sees it as a same-origin request. No CORS error possible. โ
โ ๏ธ Restart
ng serveafter changingproxy.conf.jsonโ the proxy config is only read at startup.
๐ก CORS vs Proxy โ which to use? Use the proxy for local development (simpler, no CORS config needed). Keep the CORS configuration in
Program.csfor production deployments where frontend and backend are on different domains.
โถ๏ธ Part 4: Running both servers locally
Option A: Two terminal tabs (simple)
Terminal 1 โ .NET API:
cd fullstack-app/backend
dotnet run
# API running on http://localhost:5000
Terminal 2 โ Angular:
cd fullstack-app/frontend
ng serve
# Angular running on http://localhost:4200
Open http://localhost:4200 โ you should see your products loaded from the .NET API! ๐
Option B: Single command with concurrently (recommended)
Install concurrently at the root level:
cd fullstack-app
npm init -y
npm install concurrently --save-dev
Add a package.json script:
{
"scripts": {
"start": "concurrently \"dotnet run --project backend\" \"ng serve --project-name frontend\"",
"start:api": "dotnet run --project backend",
"start:frontend": "cd frontend && ng serve"
}
}
Now one command starts everything:
npm start
Both servers boot, logs are colour-coded in the same terminal, and killing the process stops both. ๐
๐ Verifying the connection
Open your browser's Network tab (F12 โ Network). Reload the page and look for:
- โ
Request to
/api/productsโ Status200 OK - โ Response body contains your product array
- โ
No
CORSerrors in the console
If you see a CORS error, check:
- ๐ข Is
app.UseCors()inProgram.csbeforeapp.UseAuthorization()? - ๐ Does the allowed origin in
Program.csexactly match your Angular dev URL? (check port and protocol) - ๐ Did you restart both servers after making config changes?
- ๐ Are you using HTTPS on the .NET side? If so, set
"secure": falseinproxy.conf.json
Common error and fix in Program.cs:
// โ Wrong order โ CORS won't work
app.UseAuthorization();
app.UseCors("AllowAngularApp"); // too late!
// โ
Correct order
app.UseCors("AllowAngularApp"); // FIRST
app.UseAuthorization();
๐ Full setup checklist
Backend (.NET):
- ๐๏ธ
dotnet new webapi -n backend --use-controllers - ๐ฆ Create
Models/Product.cs - ๐ฎ Create
Controllers/ProductsController.cs - ๐ Add CORS policy in
Program.cs - โ ๏ธ Verify
UseCors()is beforeUseAuthorization() - ๐ Store allowed origins in
appsettings.Development.json - โ
Test
http://localhost:5000/api/productsin browser
Frontend (Angular):
- ๐
ng new frontend --standalone --routing=true - ๐ Create
environment.tsandenvironment.development.ts - ๐ Add
provideHttpClient(withFetch())toapp.config.ts - ๐ Create
Productinterface inmodels/ - ๐ง Create
ProductServicewith error handling - ๐ผ๏ธ Create
ProductsComponentwith signal-based state - ๐ฃ๏ธ Register route in
app.routes.ts - โก (Optional) Set up
proxy.conf.jsonfor dev proxy - โ
Test
http://localhost:4200shows products from API
๐ What you've built
You now have a working full-stack Angular + .NET Core application:
- ๐ฅ๏ธ .NET 9 Web API with a Products controller, proper routing, and CORS configured
- ๐ Angular 21 frontend with standalone components, signals-based state, typed models, and clean HttpClient service
- ๐ CORS properly configured for production deployments
- โก Dev proxy for seamless local development
- ๐ Both servers running together with a single npm command
This is the foundation. From here, the natural next steps are adding a database with Entity Framework Core, implementing JWT authentication, and deploying to Azure. Those posts are coming โ stay tuned! ๐