Building a REST API with ASP.NET Core

Build a small, testable API with typed requests, validation, dependency injection, and consistent HTTP responses.

Sequential code and deployment steps for an ASP.NET Core tutorial

This tutorial builds a small tasks API with ASP.NET Core minimal APIs. The result is intentionally compact, but it keeps HTTP concerns separate from storage so the design can evolve.

Prerequisites

You need the .NET SDK and a terminal. Confirm the SDK is available with dotnet --version.

Create the project

dotnet new web -n TasksApi
cd TasksApi

The empty template provides routing, dependency injection, configuration, and logging without adding features we do not need.

Define the models

Create Models/TaskItem.cs:

namespace TasksApi.Models;

public sealed record TaskItem(Guid Id, string Title, bool IsComplete);
public sealed record CreateTaskRequest(string Title);

The response has an ID assigned by the application. The request contains only the value a caller may provide.

Add an in-memory repository

Create Data/TaskRepository.cs:

using TasksApi.Models;

namespace TasksApi.Data;

public sealed class TaskRepository
{
    private readonly List<TaskItem> _items = [];

    public IReadOnlyList<TaskItem> GetAll() => _items;

    public TaskItem? Get(Guid id) =>
        _items.FirstOrDefault(item => item.Id == id);

    public TaskItem Add(string title)
    {
        var item = new TaskItem(Guid.NewGuid(), title, false);
        _items.Add(item);
        return item;
    }
}

This store is suitable for learning, not persistent data. Restarting the application clears it.

Map the endpoints

Replace Program.cs with:

using TasksApi.Data;
using TasksApi.Models;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton<TaskRepository>();

var app = builder.Build();
var tasks = app.MapGroup("/api/tasks");

tasks.MapGet("/", (TaskRepository repository) =>
    Results.Ok(repository.GetAll()));

tasks.MapGet("/{id:guid}", (Guid id, TaskRepository repository) =>
    repository.Get(id) is { } item
        ? Results.Ok(item)
        : Results.NotFound());

tasks.MapPost("/", (CreateTaskRequest request, TaskRepository repository) =>
{
    var title = request.Title?.Trim();
    if (string.IsNullOrWhiteSpace(title))
    {
        return Results.ValidationProblem(new Dictionary<string, string[]>
        {
            [nameof(request.Title)] = ["A title is required."]
        });
    }

    var item = repository.Add(title);
    return Results.Created($"/api/tasks/{item.Id}", item);
});

app.Run();

The GUID route constraint rejects malformed identifiers before the handler runs. The POST endpoint trims input, returns a validation response for an empty title, and uses 201 Created with the new resource location.

Run and call the API

Start it with dotnet run, then use the address printed in the terminal. Your port may differ from this example:

curl -X POST http://localhost:5000/api/tasks \
  -H "Content-Type: application/json" \
  -d '{"title":"Deploy the API"}'

curl http://localhost:5000/api/tasks

Next steps

The API now has explicit contracts, validation, dependency injection, a route group, and appropriate status codes. Good next steps are endpoint tests, persistent storage, structured error handling, and authentication when the application actually needs an identity boundary.