Your First Minimal API

A minimal API maps each address and method straight to a lambda in Program.cs, with MapGet, MapPost, MapPut and MapDelete. The lambda's parameters come from the route and the body, and Results.NotFound(), Results.Created() and friends choose the status code.

Apprentice · ASP.NET Core · REST API · Updated

After this lesson you can

  • Map GET, POST, PUT and DELETE endpoints to lambdas in a minimal API
  • Return a status code with Results, such as Results.NotFound() or Results.Created()

Key takeaways

  • A minimal API maps each method and address to a lambda in Program.cs with MapGet, MapPost, MapPut or MapDelete.
  • A parameter such as int id gets its value from {id} in the address; a class parameter such as Todo todo comes from the JSON body.
  • Return an object or a list, and it's sent as JSON with 200 OK.
  • Return a Results method, such as Results.NotFound(), Results.Created(...) or Results.NoContent(), to answer with a different status code.
  • ASP.NET Core writes JSON property names in camelCase, so Title becomes title.

A Web API is a web app that answers with data instead of pages. A browser asks an MVC app for /products and gets back HTML to show. A program asks an API for /api/products and gets back JSON to use. Mobile apps, JavaScript in web pages, and other servers are all clients of APIs.

ASP.NET Core has two ways to build one. This lesson shows the smallest: a minimal API.

Creating the project

In Visual Studio, create a new project from the ASP.NET Core Web API template, and on the last page clear the Use controllers box. (In a terminal: dotnet new webapi -o TodoApi.) The template's Program.cs holds a sample weather forecast; replace it with the to-do list below.

A to-do list API

Here's a whole API in Program.cs: a to-do list kept in memory, with an endpoint for each thing a client can do.

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddOpenApi();

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.MapOpenApi();
}

app.UseHttpsRedirection();

// The to-do list, kept in memory: it starts again whenever the app restarts.
List<Todo> todos = new List<Todo>
{
    new Todo { Id = 1, Title = "Buy milk", Done = false },
    new Todo { Id = 2, Title = "Call the dentist", Done = true }
};

app.MapGet("/todos", () => todos);

app.MapGet("/todos/{id}", (int id) =>
{
    Todo? todo = todos.Find(t => t.Id == id);
    if (todo == null)
    {
        return Results.NotFound();
    }
    return Results.Ok(todo);
});

app.MapPost("/todos", (Todo todo) =>
{
    todo.Id = todos.Count == 0 ? 1 : todos.Max(t => t.Id) + 1;
    todos.Add(todo);
    return Results.Created(quot;/todos/{todo.Id}", todo);
});

app.MapPut("/todos/{id}", (int id, Todo changed) =>
{
    Todo? todo = todos.Find(t => t.Id == id);
    if (todo == null)
    {
        return Results.NotFound();
    }
    todo.Title = changed.Title;
    todo.Done = changed.Done;
    return Results.Ok(todo);
});

app.MapDelete("/todos/{id}", (int id) =>
{
    int removed = todos.RemoveAll(t => t.Id == id);
    return removed == 0 ? Results.NotFound() : Results.NoContent();
});

app.Run();

public class Todo
{
    public int Id { get; set; }
    public string Title { get; set; } = "";
    public bool Done { get; set; }
}

Endpoints

Each Map... call creates an endpoint: a method and an address, joined to the code that answers it.

Call Answers
app.MapGet("/todos", ...) GET /todos
app.MapGet("/todos/{id}", ...) GET /todos/2, GET /todos/7, …
app.MapPost("/todos", ...) POST /todos
app.MapPut("/todos/{id}", ...) PUT /todos/2
app.MapDelete("/todos/{id}", ...) DELETE /todos/2

The code that answers is a lambda, like the ones you gave to LINQ. ASP.NET Core fills in its parameters for you:

  • int id comes from the {id} in the address, converted to an int: /todos/2 gives id = 2.
  • Todo todo comes from the request's JSON body, turned into a Todo object. That's called model binding, as in MVC.

Whatever the lambda returns becomes the response. Return a list or an object, as the first MapGet does, and it's sent as JSON with 200 OK.

Choosing the status code with Results

When 200 OK isn't the right answer, return one of the Results methods instead:

Return Answers
Results.Ok(todo) 200 OK, with the to-do as JSON, as the MapPut answers with the changed to-do
Results.Created("/todos/3", todo) 201 Created, with a Location header and the new to-do
Results.NoContent() 204 No Content: done, with nothing to send back, as after a DELETE
Results.NotFound() 404 Not Found
Results.BadRequest("Give the to-do a title") 400 Bad Request, with a message

Trying it

Run the app with Ctrl+F5, and send these requests from the project's .http file (use your project's own address):

@HostAddress = http://localhost:5248

GET {{HostAddress}}/todos/9

###

POST {{HostAddress}}/todos
Content-Type: application/json

{
  "title": "Water the plants",
  "done": false
}

###

The first answers 404 Not Found: there's no to-do 9. The second answers:

HTTP/1.1 201 Created
Content-Type: application/json; charset=utf-8
Location: /todos/3

{
  "id": 3,
  "title": "Water the plants",
  "done": false
}

JSON property names start with a small letter (title, done): ASP.NET Core converts C#'s Title to the camelCase that JavaScript clients expect, and back again when it reads a body.

Stop the app and start it again, and the new to-do is gone: the list only lives in memory. Real APIs keep their data in a database, as the next topic's do.

Check yourself

2 questions to try on the lesson page.

So what

A minimal API is an address, a method and a lambda, repeated for each thing a client can do. It's the quickest way to put data on the web, and the same ideas, endpoints, model binding and status codes, carry straight over to controllers.

Key terms

Cheat sheets