# 🚀 Building a Full-Stack App with Angular + .NET Core Web API: The Complete Beginner's Guide

---

## 🤔 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** ✅ — `dotnet` CLI 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 `Products` endpoint
- 🌐 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:

```bash
# 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](https://dotnet.microsoft.com/download)
- Node.js → [nodejs.org](https://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:

```bash
mkdir fullstack-app && cd fullstack-app
```

---

## 🖥️ Part 1: Building the .NET Core Web API

### Step 1: Create the Web API project

```bash
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:

```bash
mkdir Models
```

Create `Models/Product.cs`:

```csharp
// 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.Models` syntax — 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`:

```csharp
// 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:

```csharp
// 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 **before** `app.UseAuthentication()` and `app.UseAuthorization()` in `Program.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:

```json
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedOrigins": [
    "http://localhost:4200"
  ]
}
```

And update `Program.cs` to read from config instead of hard-coding:

```csharp
// 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 🚀

```bash
# 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:

```bash
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`:

```typescript
// 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`:

```typescript
// 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.ts` during `ng serve` and `environment.ts` during `ng build`. You never need to switch manually.

### Step 9: Configure HttpClient in app.config.ts

Open `src/app/app.config.ts` and add `provideHttpClient()`:

```typescript
// 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 browser `fetch()` API under the hood instead of `XMLHttpRequest`. 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`:

```typescript
// 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`:

```typescript
// 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:

```bash
ng generate component products
```

Update `src/app/products/products.component.ts`:

```typescript
// 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`:

```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`:

```typescript
// 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:

```html
<!-- 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:

```json
{
  "/api": {
    "target": "http://localhost:5000",
    "secure": false,
    "changeOrigin": true,
    "logLevel": "info"
  }
}
```

Register it in `angular.json` under `serve > options`:

```json
"serve": {
  "builder": "@angular-devkit/build-angular:dev-server",
  "options": {
    "proxyConfig": "proxy.conf.json"
  }
}
```

Now update `environment.development.ts` to use a relative URL:

```typescript
// 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 serve` after changing `proxy.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.cs` for 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:**

```bash
cd fullstack-app/backend
dotnet run
# API running on http://localhost:5000
```

**Terminal 2 — Angular:**

```bash
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:

```bash
cd fullstack-app
npm init -y
npm install concurrently --save-dev
```

Add a `package.json` script:

```json
{
  "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:

```bash
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` → Status `200 OK`
- ✅ Response body contains your product array
- ✅ No `CORS` errors in the console

If you see a CORS error, check:

1. 🔢 Is `app.UseCors()` in `Program.cs` **before** `app.UseAuthorization()`?
2. 🌐 Does the allowed origin in `Program.cs` exactly match your Angular dev URL? (check port and protocol)
3. 🔄 Did you restart both servers after making config changes?
4. 🔒 Are you using HTTPS on the .NET side? If so, set `"secure": false` in `proxy.conf.json`

Common error and fix in `Program.cs`:

```csharp
// ❌ 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 before `UseAuthorization()`
- [ ] 📄 Store allowed origins in `appsettings.Development.json`
- [ ] ✅ Test `http://localhost:5000/api/products` in browser

**Frontend (Angular):**
- [ ] 🌐 `ng new frontend --standalone --routing=true`
- [ ] 🌍 Create `environment.ts` and `environment.development.ts`
- [ ] 💉 Add `provideHttpClient(withFetch())` to `app.config.ts`
- [ ] 📐 Create `Product` interface in `models/`
- [ ] 🔧 Create `ProductService` with error handling
- [ ] 🖼️ Create `ProductsComponent` with signal-based state
- [ ] 🛣️ Register route in `app.routes.ts`
- [ ] ⚡ (Optional) Set up `proxy.conf.json` for dev proxy
- [ ] ✅ Test `http://localhost:4200` shows 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! 👀

---

## 📚 Further reading

- 📖 [ASP.NET Core Web API docs](https://docs.microsoft.com/en-us/aspnet/core/web-api)
- 🔒 [CORS in ASP.NET Core — official docs](https://docs.microsoft.com/en-us/aspnet/core/security/cors)
- ⚡ [Angular HttpClient guide](https://angular.dev/guide/http)
- 🌍 [Angular environment configuration](https://angular.dev/guide/build#configuring-application-environments)
- 🔄 [Angular standalone migration guide](https://blog.techtush.in/migrating-from-ngmodules-to-angular-standalone-components)
