Skip to main content

Command Palette

Search for a command to run...

๐Ÿš€ 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

Published
โ€ข14 min read
๐Ÿš€ Building a Full-Stack App with Angular + .NET Core Web API: The Complete Beginner's Guide
T

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 โœ… โ€” 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:

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

๐Ÿ’ก 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.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:

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

{
  "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.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():

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

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

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! ๐ŸŽ‰

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 โ†’ 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:

// โŒ 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