Full-Stack Task Tracker: Angular + .NET
Build a task tracker app connecting an Angular frontend to a .NET Web API backend.
Introduction
30 Second Summary
Every web app you use daily has two sides talking to each other. A frontend displays the buttons and lists you see, while a backend stores and manages the data behind the scenes. The magic happens when these two sides communicate over the network.
In this project, you will build a full-stack task tracker application with a .NET Web API backend and an Angular frontend that talk to each other over HTTP. You will create endpoints, fetch data, and handle the real-world challenges of connecting two separate applications.
What You'll Build
A task tracker with a REST API backend that serves task data and an Angular frontend that displays, creates, and deletes tasks by calling that API.
By the end of this project, you'll have:
- A .NET 8 Web API project with a controller serving RESTful endpoints for managing tasks.
- An Angular application that fetches, creates, and deletes tasks by communicating with your API using HttpClient.
- A configured CORS policy that allows your frontend and backend to communicate across different ports.
- Secret Mission: Add a PUT endpoint and implement inline task editing in the Angular UI.
Are there any prerequisites?
This project assumes basic familiarity with HTML, CSS, TypeScript, and C#. You will also need Node.js (v20.19.0+), .NET SDK (8.x), Angular CLI, and a code editor installed on your machine.
Scaffold Both Projects
Every full-stack application has two halves: a backend that stores and serves data, and a frontend that users interact with. These two halves live in separate project folders and communicate over HTTP.
In this step, you will scaffold both projects from scratch using their official CLI tools. By the end, you will have a .NET Web API and an Angular app running independently on your machine.
In this step, get ready to:
- Create the .NET Web API backend project.
- Create the Angular frontend project.
- Verify both projects build and run independently.
Create the .NET Web API project
The .NET CLI scaffolds a full API project from a template. You will use the controller-based template, which organizes your API endpoints into separate controller classes.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create the .NET Web API project by running this command:
dotnet new webapi -controllers -f net8.0 -n TaskTrackerApi
What does this command do?
- dotnet new webapi scaffolds a new Web API project from a template.
- -controllers uses the controller-based pattern instead of minimal APIs.
- -f net8.0 targets .NET 8, the current LTS release.
- -n TaskTrackerApi names the project and its output folder.
- Confirm the project folder was created by running:
ls
You should see a TaskTrackerApi folder in the output.
Don't see the folder?
- Make sure you ran the command from your Desktop directory. Run pwd to check your current location.
- Check that your .NET SDK is installed correctly by running dotnet --version. You should see a version starting with 8. in the output.
Help me troubleshoot why my dotnet new command failed
- Navigate into the project folder by running:
cd TaskTrackerApi
The scaffold includes example files that you do not need for this project. You will delete them now to keep the project clean.
- Delete the scaffolded example files by running these commands:
rm WeatherForecast.cs
rm Controllers/WeatherForecastController.cs
Why delete these files?
The .NET Web API template ships with a sample WeatherForecast endpoint to show the project structure. You will replace it with your own task endpoints in the next step.
Using Command Prompt on Windows?
If you are using Command Prompt instead of PowerShell, replace rm with del and use backslashes for the path. PowerShell and Git Bash both support rm as shown above.
- Verify the project still builds cleanly by running:
dotnet build
You should see a success message confirming the build completed with 0 errors.
Seeing a build error?
- Make sure you deleted both files. Run ls and confirm WeatherForecast.cs is gone from the root. Run ls Controllers and confirm WeatherForecastController.cs is gone.
- If you accidentally deleted Program.cs or the .csproj file, delete the entire TaskTrackerApi folder and re-run the dotnet new command from your Desktop.
Help me fix my .NET build error after deleting the WeatherForecast files
Create the Angular project
The Angular CLI scaffolds a complete frontend application with a development server, build tooling, and a component structure ready to go.
- Navigate back to your Desktop by running:
cd ~/Desktop
- Create the Angular project by running this command:
ng new task-tracker --style=css --ssr=false
What does this command do?
- ng new task-tracker creates a new Angular workspace and application named task-tracker.
- --style=css sets plain CSS as the stylesheet format.
- --ssr=false disables server-side rendering, which you do not need for a local development project.
This command also installs all npm dependencies automatically, so it may take a minute or two to finish.
Once the installation finishes, you should see a confirmation message and be returned to your terminal prompt.
- Navigate into the project folder by running:
cd task-tracker
- Start the Angular development server by running:
npm start
Your terminal should display output mentioning localhost:4200. Open your browser and navigate to http://localhost:4200 to see the Angular welcome page.
Not seeing the welcome page?
- Open your browser manually and navigate to http://localhost:4200.
- If you see an error about port 4200 being in use, close any other development servers and try npm start again.
- Make sure the terminal shows that the server is running. Look for output that includes localhost:4200.
Help me troubleshoot why my Angular dev server is not working
- Press Ctrl+C in the terminal to stop the development server.
Verify your workspace structure
- Navigate back to your Desktop by running:
cd ~/Desktop
- Confirm both project folders exist by running:
ls
You should see both TaskTrackerApi and task-tracker listed as sibling folders on your Desktop.
Why two separate folders?
Keeping the frontend and backend in separate projects mirrors how real teams organize full-stack applications. Each project has its own dependencies, build process, and deployment lifecycle.
They communicate over HTTP, which means you could swap either side without touching the other.
Both projects are scaffolded and ready to go. Next up, you will build the API endpoints that serve task data from the .NET backend.
Build the .NET API Endpoints
Both projects are scaffolded and building. Now it's time to give the backend something useful to do.
A REST API exposes endpoints that clients call over HTTP. In this step, you'll define a task model, build controller endpoints for managing tasks, and configure CORS so the Angular dev server can reach the API.
In this step, get ready to:
- Create a TaskItem model and a TasksController with CRUD endpoints.
- Configure CORS to allow requests from the Angular dev server.
- Test the API by requesting data in the browser.
Create the task model
Every API needs a data shape that both the server and client agree on. In .NET, this is a C# class with properties that map to JSON fields when sent over HTTP.
- Navigate into the API project by running this command:
cd TaskTrackerApi
- Create the Models folder by running:
mkdir Models
- Create a new file at Models/TaskItem.cs and paste the following code:
namespace TaskTrackerApi.Models;
public class TaskItem
{
// Unique identifier for each task
public int Id { get; set; }
// The task's display text
public string Title { get; set; } = string.Empty;
// Whether the task has been completed
public bool IsComplete { get; set; }
}
What does this code do?
This class defines the shape of a task. Each property maps directly to a field in the JSON the API sends and receives.
- Id is a unique number assigned to each task.
- Title holds the task's display text, defaulting to an empty string.
- IsComplete tracks whether the task is done.
- Save Models/TaskItem.cs.
File not saving?
Make sure you created the Models folder first. The file path should be TaskTrackerApi/Models/TaskItem.cs.
Help me troubleshoot my file path
Create the tasks controller
A controller is a C# class that handles incoming HTTP requests and returns responses. Each public method decorated with an HTTP attribute becomes an API endpoint.
- Create a new file at Controllers/TasksController.cs and paste the following code:
using Microsoft.AspNetCore.Mvc;
using TaskTrackerApi.Models;
namespace TaskTrackerApi.Controllers;
[ApiController]
[Route("api/[controller]")]
public class TasksController : ControllerBase
{
// In-memory list of tasks (resets when the app restarts)
private static readonly List<TaskItem> Tasks = new()
{
new TaskItem { Id = 1, Title = "Learn .NET Web API", IsComplete = false },
new TaskItem { Id = 2, Title = "Learn Angular", IsComplete = false },
new TaskItem { Id = 3, Title = "Build a full-stack app", IsComplete = true }
};
// GET /api/tasks - return all tasks
[HttpGet]
public ActionResult<List<TaskItem>> GetAll()
{
return Ok(Tasks);
}
// GET /api/tasks/{id} - return a single task by ID
[HttpGet("{id}")]
public ActionResult<TaskItem> GetById(int id)
{
var task = Tasks.FirstOrDefault(t => t.Id == id);
if (task == null) return NotFound();
return Ok(task);
}
// POST /api/tasks - create a new task
[HttpPost]
public ActionResult<TaskItem> Create(TaskItem task)
{
task.Id = Tasks.Count > 0 ? Tasks.Max(t => t.Id) + 1 : 1;
Tasks.Add(task);
return CreatedAtAction(nameof(GetById), new { id = task.Id }, task);
}
// DELETE /api/tasks/{id} - remove a task
[HttpDelete("{id}")]
public ActionResult Delete(int id)
{
var task = Tasks.FirstOrDefault(t => t.Id == id);
if (task == null) return NotFound();
Tasks.Remove(task);
return NoContent();
}
}
What does this code do?
This controller serves four endpoints, all routed under /api/tasks:
- GetAll() returns every task in the list.
- GetById(id) returns a single task matching the given ID.
- Create(task) adds a new task with an auto-assigned ID.
- Delete(id) removes a task from the list.
The static list stores tasks in memory. Every time the API restarts, the data resets to the three seed tasks.
- Save Controllers/TasksController.cs.
- Confirm the project still compiles by running:
dotnet build
You should see a Build succeeded message with 0 errors.
Seeing build errors?
Check that your TaskItem.cs file is inside the Models/ folder and that the namespace matches TaskTrackerApi.Models. The controller imports this namespace with using TaskTrackerApi.Models;.
Help me troubleshoot my build errors
✔️ Awesome, I've got everything!
Great. Make sure both files are saved before moving on.
ⓧ I'd like to double check the full code
Here is the full Controllers/TasksController.cs:
using Microsoft.AspNetCore.Mvc;
using TaskTrackerApi.Models;
namespace TaskTrackerApi.Controllers;
[ApiController]
[Route("api/[controller]")]
public class TasksController : ControllerBase
{
private static readonly List<TaskItem> Tasks = new()
{
new TaskItem { Id = 1, Title = "Learn .NET Web API", IsComplete = false },
new TaskItem { Id = 2, Title = "Learn Angular", IsComplete = false },
new TaskItem { Id = 3, Title = "Build a full-stack app", IsComplete = true }
};
[HttpGet]
public ActionResult<List<TaskItem>> GetAll()
{
return Ok(Tasks);
}
[HttpGet("{id}")]
public ActionResult<TaskItem> GetById(int id)
{
var task = Tasks.FirstOrDefault(t => t.Id == id);
if (task == null) return NotFound();
return Ok(task);
}
[HttpPost]
public ActionResult<TaskItem> Create(TaskItem task)
{
task.Id = Tasks.Count > 0 ? Tasks.Max(t => t.Id) + 1 : 1;
Tasks.Add(task);
return CreatedAtAction(nameof(GetById), new { id = task.Id }, task);
}
[HttpDelete("{id}")]
public ActionResult Delete(int id)
{
var task = Tasks.FirstOrDefault(t => t.Id == id);
if (task == null) return NotFound();
Tasks.Remove(task);
return NoContent();
}
}
And here is Models/TaskItem.cs:
namespace TaskTrackerApi.Models;
public class TaskItem
{
public int Id { get; set; }
public string Title { get; set; } = string.Empty;
public bool IsComplete { get; set; }
}
Configure CORS to allow Angular requests
Your Angular app runs on http://localhost:4200 and your API will run on http://localhost:5000. Browsers block requests between different origins by default. This security feature is called CORS (Cross-Origin Resource Sharing).
You need to tell the API to explicitly allow requests from the Angular dev server's origin.
- Open Program.cs in the TaskTrackerApi folder and replace its entire contents with the following:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
// Allow the Angular dev server to call this API
builder.Services.AddCors(options =>
{
options.AddPolicy("AllowAngular", policy =>
{
policy.WithOrigins("http://localhost:4200")
.AllowAnyHeader()
.AllowAnyMethod();
});
});
var app = builder.Build();
app.UseHttpsRedirection();
// CORS middleware must come before UseAuthorization
app.UseCors("AllowAngular");
app.UseAuthorization();
app.MapControllers();
app.Run();
What does this code do?
AddCors registers a named policy called AllowAngular. This policy tells the API to accept requests from http://localhost:4200 with any header and any HTTP method.
UseCors("AllowAngular") activates the policy in the middleware pipeline. It must appear before UseAuthorization so that preflight requests are handled before authorization checks.
Why does middleware order matter?
ASP.NET Core processes middleware in the order you register it. If UseAuthorization ran before UseCors, the browser's preflight OPTIONS request would be rejected before CORS headers are added.
- Save Program.cs.
- Confirm the project still compiles by running:
dotnet build
You should see Build succeeded again with 0 errors.
Build failing after editing Program.cs?
Make sure you replaced the entire file contents. Leftover lines from the original scaffold can cause duplicate method calls or syntax errors.
Help me fix my Program.cs build error
✔️ Awesome, I've got everything!
Great. Double check that you saved Program.cs before moving on.
ⓧ I'd like to double check the full code
Here is the full Program.cs:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddCors(options =>
{
options.AddPolicy("AllowAngular", policy =>
{
policy.WithOrigins("http://localhost:4200")
.AllowAnyHeader()
.AllowAnyMethod();
});
});
var app = builder.Build();
app.UseHttpsRedirection();
app.UseCors("AllowAngular");
app.UseAuthorization();
app.MapControllers();
app.Run();
Test the API
Everything compiles. Time to run the API and confirm it serves real data.
- Start the API on a fixed port by running:
dotnet run --urls "http://localhost:5000"
Why specify the port?
The .NET scaffold assigns a random port by default. Using --urls "http://localhost:5000" forces a predictable address that the Angular app can reliably call.
- Open your browser and navigate to http://localhost:5000/api/tasks.
You should see a JSON array with three seed tasks displayed in the browser.
Seeing a connection error?
Make sure the dotnet run command is still running in your terminal. If you closed it or pressed Ctrl+C, restart with the same command.
Also confirm you are using http:// (not https://) in the browser URL.
Help me debug my API connection
Your API is live and serving task data. Next up, you'll connect the Angular frontend to consume these endpoints and display tasks in the browser.
Connect Angular to the API
Your API is serving task data at http://localhost:5000/api/tasks. Now you need a way for the Angular frontend to fetch that data and display it.
Angular uses a built-in HttpClient service to make HTTP requests. You'll configure it in the app, create a dedicated service to talk to the API, and then wire up the component to display the results.
In this step, get ready to:
- Configure Angular's HttpClient so it can make HTTP requests.
- Create a TaskService that wraps all API calls in one place.
- Display tasks from the .NET API in your Angular component.
Configure HttpClient in Angular
Before Angular can make any HTTP request, you need to register the HttpClient provider. This happens in your app's configuration file.
- In your code editor, open src/app/app.config.ts inside the task-tracker folder.
- Replace the entire file contents with the following code:
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
import { provideHttpClient } from '@angular/common/http';
export const appConfig: ApplicationConfig = {
providers: [
provideZoneChangeDetection({ eventCoalescing: true }),
provideHttpClient()
]
};
What does this code do?
The provideHttpClient() function registers Angular's HTTP service so any component or service can inject HttpClient and make requests.
The provideZoneChangeDetection line was already there from the scaffold. It handles Angular's change detection. You added provideHttpClient() right after it.
- Save app.config.ts.
Next, you need a TypeScript interface that matches the shape of the data your API returns. This gives you type safety when working with task objects.
- Create a new folder called models inside src/app/.
- Create a new file called task-item.ts inside src/app/models/.
- Add the following interface definition:
export interface TaskItem {
id: number;
title: string;
isComplete: boolean;
}
What does this interface do?
This interface defines the exact shape of a task object. It matches the three properties from your C# TaskItem model: Id, Title, and IsComplete.
Notice the property names use camelCase (isComplete) instead of PascalCase (IsComplete). .NET automatically converts PascalCase C# properties to camelCase when serializing to JSON.
- Save task-item.ts.
- Confirm you now have two new paths: src/app/app.config.ts (updated) and src/app/models/task-item.ts (new).
File not saving?
Make sure you created the models folder inside src/app/, not in the project root. The full path should be task-tracker/src/app/models/task-item.ts.
Help me figure out why my file isn't in the right location.
Create the TaskService
Angular uses services to centralize data-fetching logic. Instead of making HTTP calls directly from your component, you create a service that handles all API communication. This keeps your components focused on displaying data.
- Create a new folder called services inside src/app/.
- Create a new file called task.service.ts inside src/app/services/.
- Add the following service code:
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { TaskItem } from '../models/task-item';
@Injectable({ providedIn: 'root' })
export class TaskService {
private http = inject(HttpClient);
private apiUrl = 'http://localhost:5000/api/tasks';
getTasks(): Observable<TaskItem[]> {
return this.http.get<TaskItem[]>(this.apiUrl);
}
addTask(task: { title: string; isComplete: boolean }): Observable<TaskItem> {
return this.http.post<TaskItem>(this.apiUrl, task);
}
deleteTask(id: number): Observable<void> {
return this.http.delete<void>(`${this.apiUrl}/${id}`);
}
}
What does this service do?
The @Injectable({ providedIn: 'root' }) decorator makes this service available everywhere in your app through dependency injection.
The inject(HttpClient) function gives the service access to Angular's HTTP client. Each method (getTasks, addTask, deleteTask) returns an Observable. Observables are like promises that can emit multiple values over time.
The apiUrl points to your .NET API at http://localhost:5000/api/tasks. Every method builds on this base URL to call the correct endpoint.
- Save task.service.ts.
Seeing red squiggly lines in your editor?
If your editor shows import errors on HttpClient or Observable, make sure you saved app.config.ts in the previous substep. Also check that the import path for TaskItem uses '../models/task-item' (going up one folder level).
Help me fix the import errors in my task service.
Display tasks in the AppComponent
The service is ready but nothing will happen until a component subscribes to its methods. You'll update the root AppComponent to inject the service, fetch tasks when the component loads, and render them in the template.
- Open src/app/app.ts in the task-tracker project.
- Replace the entire file contents with the following code:
import { Component, signal, inject, OnInit } from '@angular/core';
import { TaskService } from './services/task.service';
import { TaskItem } from './models/task-item';
@Component({
selector: 'app-root',
template: `
<main>
<h1>Task Tracker</h1>
@if (loading()) {
<p>Loading tasks...</p>
}
<ul class="task-list">
@for (task of tasks(); track task.id) {
<li [class.complete]="task.isComplete">
<span>{{ task.title }}</span>
</li>
}
</ul>
</main>
`,
styles: `
main {
max-width: 600px;
margin: 2rem auto;
font-family: system-ui, sans-serif;
padding: 0 1rem;
}
h1 {
color: #333;
}
.task-list {
list-style: none;
padding: 0;
}
.task-list li {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.75rem;
border-bottom: 1px solid #eee;
}
.task-list li.complete span {
text-decoration: line-through;
color: #999;
}
`
})
export class AppComponent implements OnInit {
private taskService = inject(TaskService);
tasks = signal<TaskItem[]>([]);
loading = signal(false);
ngOnInit(): void {
this.loadTasks();
}
loadTasks(): void {
this.loading.set(true);
this.taskService.getTasks().subscribe({
next: (tasks) => {
this.tasks.set(tasks);
this.loading.set(false);
},
error: () => this.loading.set(false)
});
}
}
What does this code do?
The inject(TaskService) line uses dependency injection to get an instance of your service. The tasks and loading variables are Angular signals. Signals automatically update the template whenever their value changes.
The ngOnInit lifecycle hook runs once when the component first loads. It calls loadTasks(), which subscribes to the getTasks() observable. When the API response arrives, it stores the tasks in the signal.
The @for block in the template loops over the tasks signal and renders a list item for each one. The track task.id part helps Angular efficiently update the DOM when the list changes.
- Save app.ts.
- Make sure your .NET API is still running in a separate terminal at http://localhost:5000. If it stopped, restart it by navigating to the TaskTrackerApi folder and running:
dotnet run --urls "http://localhost:5000"
- Switch back to your task-tracker terminal. If the Angular dev server is not running, start it with:
npm start
- Open http://localhost:4200 in your browser. You should see a list of 3 tasks: "Learn .NET Web API", "Learn Angular", and "Build a full-stack app" (with the last one showing strikethrough text because it is marked as complete).
Not seeing any tasks?
Check that your .NET API is running. Open http://localhost:5000/api/tasks directly in your browser. If you see JSON data there but the Angular app shows nothing, open your browser's developer console (F12) and look for a CORS error.
A CORS error means your .NET API is not allowing requests from http://localhost:4200. Double-check that Program.cs has the AllowAngular CORS policy with the correct origin and that app.UseCors("AllowAngular") appears before app.UseAuthorization().
Help me debug why my Angular app can't connect to the .NET API.
✔️ Awesome, I've got everything!
Great. Double check you have saved all three files: app.config.ts, task-item.ts, and task.service.ts.
ⓧ I'd like to double check the full code
Here is the full code for each file you created or modified in this step.
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
import { provideHttpClient } from '@angular/common/http';
export const appConfig: ApplicationConfig = {
providers: [
provideZoneChangeDetection({ eventCoalescing: true }),
provideHttpClient()
]
};
export interface TaskItem {
id: number;
title: string;
isComplete: boolean;
}
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { TaskItem } from '../models/task-item';
@Injectable({ providedIn: 'root' })
export class TaskService {
private http = inject(HttpClient);
private apiUrl = 'http://localhost:5000/api/tasks';
getTasks(): Observable<TaskItem[]> {
return this.http.get<TaskItem[]>(this.apiUrl);
}
addTask(task: { title: string; isComplete: boolean }): Observable<TaskItem> {
return this.http.post<TaskItem>(this.apiUrl, task);
}
deleteTask(id: number): Observable<void> {
return this.http.delete<void>(`${this.apiUrl}/${id}`);
}
}
import { Component, signal, inject, OnInit } from '@angular/core';
import { TaskService } from './services/task.service';
import { TaskItem } from './models/task-item';
@Component({
selector: 'app-root',
template: `
<main>
<h1>Task Tracker</h1>
@if (loading()) {
<p>Loading tasks...</p>
}
<ul class="task-list">
@for (task of tasks(); track task.id) {
<li [class.complete]="task.isComplete">
<span>{{ task.title }}</span>
</li>
}
</ul>
</main>
`,
styles: `
main {
max-width: 600px;
margin: 2rem auto;
font-family: system-ui, sans-serif;
padding: 0 1rem;
}
h1 {
color: #333;
}
.task-list {
list-style: none;
padding: 0;
}
.task-list li {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.75rem;
border-bottom: 1px solid #eee;
}
.task-list li.complete span {
text-decoration: line-through;
color: #999;
}
`
})
export class AppComponent implements OnInit {
private taskService = inject(TaskService);
tasks = signal<TaskItem[]>([]);
loading = signal(false);
ngOnInit(): void {
this.loadTasks();
}
loadTasks(): void {
this.loading.set(true);
this.taskService.getTasks().subscribe({
next: (tasks) => {
this.tasks.set(tasks);
this.loading.set(false);
},
error: () => this.loading.set(false)
});
}
}
Your Angular frontend is now pulling live data from the .NET backend. Next up, you'll add the ability to create and delete tasks directly from the UI.
Complete the CRUD Operations
Your Angular app is talking to the .NET API and displaying the seed tasks. That proves the HTTP connection works.
Now you will make the app fully interactive by adding an input form for new tasks and a delete button for each existing task. By the end of this step, you will have a working task tracker that can create, read, and delete tasks in real time.
In this step, get ready to:
- Add a form that sends new tasks to the API.
- Add delete buttons that remove tasks from the API.
- Style the UI so the task list is clean and readable.
Build the complete app component
The app component needs a template with the form, task list, and delete buttons. It also needs class methods that call the TaskService and component styles to keep everything readable.
- Replace the entire content of src/app/app.ts with this complete component:
import { Component, signal, inject, OnInit } from '@angular/core';
import { TaskService } from './services/task.service';
import { TaskItem } from './models/task-item';
@Component({
selector: 'app-root',
template: `
<main>
<h1>Task Tracker</h1>
<div class="add-task">
<input
type="text"
placeholder="Enter a new task..."
[value]="newTaskTitle()"
(input)="newTaskTitle.set($any($event.target).value)"
(keyup.enter)="addTask()"
/>
<button (click)="addTask()">Add</button>
</div>
@if (loading()) {
<p>Loading tasks...</p>
}
<ul class="task-list">
@for (task of tasks(); track task.id) {
<li [class.complete]="task.isComplete">
<span>{{ task.title }}</span>
<button class="delete-btn" (click)="deleteTask(task.id)">Delete</button>
</li>
}
</ul>
</main>
`,
styles: `
main {
max-width: 600px;
margin: 2rem auto;
font-family: system-ui, sans-serif;
padding: 0 1rem;
}
h1 {
color: #333;
}
.add-task {
display: flex;
gap: 0.5rem;
margin-bottom: 1.5rem;
}
.add-task input {
flex: 1;
padding: 0.5rem;
border: 1px solid #ccc;
border-radius: 4px;
font-size: 1rem;
}
.add-task button {
padding: 0.5rem 1rem;
background: #4f46e5;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 1rem;
}
.task-list {
list-style: none;
padding: 0;
}
.task-list li {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.75rem;
border-bottom: 1px solid #eee;
}
.task-list li.complete span {
text-decoration: line-through;
color: #999;
}
.delete-btn {
padding: 0.25rem 0.5rem;
background: #ef4444;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
`
})
export class AppComponent implements OnInit {
private taskService = inject(TaskService);
tasks = signal<TaskItem[]>([]);
newTaskTitle = signal('');
loading = signal(false);
ngOnInit(): void {
this.loadTasks();
}
loadTasks(): void {
this.loading.set(true);
this.taskService.getTasks().subscribe({
next: (tasks) => {
this.tasks.set(tasks);
this.loading.set(false);
},
error: () => this.loading.set(false)
});
}
addTask(): void {
const title = this.newTaskTitle().trim();
if (!title) return;
this.taskService.addTask({ title, isComplete: false }).subscribe({
next: () => {
this.newTaskTitle.set('');
this.loadTasks();
}
});
}
deleteTask(id: number): void {
this.taskService.deleteTask(id).subscribe({
next: () => this.loadTasks()
});
}
}
What does this code do?
This component has three sections: template, styles, and class logic.
- The template contains the HTML the browser renders. It has an input field bound to the newTaskTitle signal, a loading message controlled by @if (loading()), and a task list with delete buttons.
- The styles section contains CSS that only applies to this component. It centers the layout, styles the form, and colours the delete buttons red.
- The class holds three signals (tasks, newTaskTitle, loading) and methods that call the TaskService to interact with the API.
- Save app.ts.
Seeing a compile error?
- Check that your import paths match exactly. The TaskService import should be './services/task.service' and TaskItem should be './models/task-item'.
- Make sure you have the backtick character (`) wrapping the template and styles strings, not regular quotes.
- Help me debug my compile error
Understand the add-task form
The form at the top of the template is what lets you create new tasks. It uses Angular's event binding to connect the input field to your component logic.
How does the form work?
- [value]="newTaskTitle()" binds the input's displayed value to the signal. When the signal changes, the input updates.
- (input)="newTaskTitle.set(...)" updates the signal every time the user types a character.
- (keyup.enter)="addTask()" fires the addTask() method when the user presses Enter instead of clicking the button.
- Inside addTask(), the method trims whitespace, sends the new task to the API, clears the input, and reloads the list.
What is subscribe?
Angular's HttpClient returns an Observable instead of a Promise. Nothing happens until you call .subscribe() on it.
Think of it like placing an order. The Observable is the order slip. Calling subscribe() is handing it to the kitchen so they actually start cooking.
Test the full CRUD flow
Each task in the list now has a red Delete button. Clicking it sends a DELETE request to the API and refreshes the list.
How does delete work?
- (click)="deleteTask(task.id)" passes the task's unique ID to the deleteTask() method.
- The method calls TaskService.deleteTask(id), which sends an HTTP DELETE to /api/tasks/{id}.
- On success, loadTasks() fires again to refresh the displayed list.
What does the loading indicator do?
The @if (loading()) block in the template shows a "Loading tasks..." message while the HTTP request is in flight. Once loadTasks() receives a response, it sets loading to false and the message disappears.
- Make sure your .NET API is still running at http://localhost:5000.
- Switch back to the browser tab showing http://localhost:4200 and refresh the page.
- You should see the 3 seed tasks displayed in a clean, styled list with red Delete buttons on each row.
- Type Buy groceries in the input field and click Add.
- The new task should appear at the bottom of the list.
- Click Delete on any task to remove it from the list.
Tasks not appearing or form not working?
- Confirm the API is running. Open http://localhost:5000/api/tasks in a new browser tab. You should see JSON.
- Check your browser's developer console (F12) for CORS errors. If you see one, make sure Program.cs allows http://localhost:4200 and that UseCors is called before UseAuthorization.
- If the Add button does nothing, check that your addTask() method calls .subscribe() on the observable. Without subscribe(), the HTTP request never fires.
- Help me troubleshoot my task tracker
✔️ Awesome, I've got everything!
Great. Double check you have saved app.ts and that both the Angular app and .NET API are running.
ⓧ I'd like to double check the full code
import { Component, signal, inject, OnInit } from '@angular/core';
import { TaskService } from './services/task.service';
import { TaskItem } from './models/task-item';
@Component({
selector: 'app-root',
template: `
<main>
<h1>Task Tracker</h1>
<div class="add-task">
<input
type="text"
placeholder="Enter a new task..."
[value]="newTaskTitle()"
(input)="newTaskTitle.set($any($event.target).value)"
(keyup.enter)="addTask()"
/>
<button (click)="addTask()">Add</button>
</div>
@if (loading()) {
<p>Loading tasks...</p>
}
<ul class="task-list">
@for (task of tasks(); track task.id) {
<li [class.complete]="task.isComplete">
<span>{{ task.title }}</span>
<button class="delete-btn" (click)="deleteTask(task.id)">Delete</button>
</li>
}
</ul>
</main>
`,
styles: `
main {
max-width: 600px;
margin: 2rem auto;
font-family: system-ui, sans-serif;
padding: 0 1rem;
}
h1 {
color: #333;
}
.add-task {
display: flex;
gap: 0.5rem;
margin-bottom: 1.5rem;
}
.add-task input {
flex: 1;
padding: 0.5rem;
border: 1px solid #ccc;
border-radius: 4px;
font-size: 1rem;
}
.add-task button {
padding: 0.5rem 1rem;
background: #4f46e5;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 1rem;
}
.task-list {
list-style: none;
padding: 0;
}
.task-list li {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.75rem;
border-bottom: 1px solid #eee;
}
.task-list li.complete span {
text-decoration: line-through;
color: #999;
}
.delete-btn {
padding: 0.25rem 0.5rem;
background: #ef4444;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
`
})
export class AppComponent implements OnInit {
private taskService = inject(TaskService);
tasks = signal<TaskItem[]>([]);
newTaskTitle = signal('');
loading = signal(false);
ngOnInit(): void {
this.loadTasks();
}
loadTasks(): void {
this.loading.set(true);
this.taskService.getTasks().subscribe({
next: (tasks) => {
this.tasks.set(tasks);
this.loading.set(false);
},
error: () => this.loading.set(false)
});
}
addTask(): void {
const title = this.newTaskTitle().trim();
if (!title) return;
this.taskService.addTask({ title, isComplete: false }).subscribe({
next: () => {
this.newTaskTitle.set('');
this.loadTasks();
}
});
}
deleteTask(id: number): void {
this.taskService.deleteTask(id).subscribe({
next: () => this.loadTasks()
});
}
}
Your task tracker is fully functional. You can create, read, and delete tasks through the Angular frontend, with every action hitting the .NET API over HTTP. Next up, you will take this further with a Secret Mission that adds editing capabilities.
Secret mission
Edit Tasks with a PUT Endpoint
Your task tracker can create and delete tasks, but what about editing them? In this secret mission, you'll add a PUT endpoint to your API and implement inline editing in Angular. Double-click a task title to rename it, or tick a checkbox to mark it complete.
Clean Up Your Resources
Clean Up Your Resources
Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
This project runs entirely locally, so there are no ongoing costs.
Resources you used:
- TaskTrackerApi/ project folder on your Desktop.
- task-tracker/ project folder on your Desktop.
- Running .NET API server at http://localhost:5000.
- Running Angular dev server at http://localhost:4200.
Keep everything running
No action needed. Choose this if you're still actively building or experimenting with the task tracker.
- Both servers continue running in their terminal windows.
- Your project files remain on the Desktop.
- You can revisit the app at http://localhost:4200 any time while the servers are running.
Pause - I'll come back to this later
Shut down the running servers to free up terminal windows and ports. Your project files stay on disk so you can restart later.
- In the terminal running the .NET API, press Ctrl+C to stop the server.
- In the terminal running the Angular dev server, press Ctrl+C to stop the server.
- To restart later, navigate back into each project folder and run the start commands again.
Delete - I don't want to use this again
Remove all project files and stop any running processes. This is a full cleanup.
- In the terminal running the .NET API, press Ctrl+C to stop the server.
- In the terminal running the Angular dev server, press Ctrl+C to stop the server.
- Delete both project folders by running these commands:
macOS
cd ~/Desktop
rm -rf TaskTrackerApi
rm -rf task-tracker
Windows
cd ~\Desktop
rmdir /s /q TaskTrackerApi
rmdir /s /q task-tracker
Nice Work!
Nice Work!
Nice work! You just built a full-stack task tracker with a .NET 8 Web API backend and an Angular frontend communicating over HTTP.
You've learned how to:
- Build a .NET 8 Web API with controller-based REST endpoints serving GET, POST, and DELETE operations.
- Configure CORS to allow cross-origin requests between your Angular dev server and the .NET API.
- Create an Angular service that communicates with the API using HttpClient and build a reactive UI that displays, creates, and deletes tasks in real-time.
- Secret Mission: Added a PUT endpoint and inline editing for a complete CRUD experience.
Ready to quiz yourself?