Section 5 of 7

Minimal APIs

🎯 What You'll Learn

  • What Minimal APIs are
  • MapGet, MapPost, MapPut, MapDelete
  • Route groups
  • Filters and middleware
  • When to use Minimal APIs vs Controllers
  • Complete CRUD example

What are Minimal APIs?

Minimal APIs are a simplified way to build HTTP APIs with minimal code and configuration. Introduced in .NET 6, they reduce ceremony and boilerplate.

Controllers vs Minimal APIs

Feature Controllers Minimal APIs
Code More boilerplate Less code
Organization Class-based Function-based
Features Full-featured Lightweight
Best For Large, complex APIs Simple, microservices

Basic Minimal API

Program.cs C#
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

// GET /hello
app.MapGet("/hello", () => "Hello World!");

// GET /products
app.MapGet("/products", () => new[] { "Laptop", "Mouse", "Keyboard" });

app.Run();

HTTP Methods

MapGet, MapPost, MapPut, MapDelete C#
// GET /api/products
app.MapGet("/api/products", () => "Get all products");

// GET /api/products/123
app.MapGet("/api/products/{id}", (int id) => $"Product {id}");

// POST /api/products
app.MapPost("/api/products", (Product product) => "Product created");

// PUT /api/products/123
app.MapPut("/api/products/{id}", (int id, Product product) => "Product updated");

// DELETE /api/products/123
app.MapDelete("/api/products/{id}", (int id) => "Product deleted");

Returning Results

Results Helpers C#
// 200 OK
app.MapGet("/products", () => Results.Ok(new[] { "Laptop" }));

// 201 Created
app.MapPost("/products", (Product p) => 
    Results.Created($"/products/{p.Id}", p));

// 204 No Content
app.MapDelete("/products/{id}", (int id) => Results.NoContent());

// 404 Not Found
app.MapGet("/products/{id}", (int id) => 
    id > 0 ? Results.Ok(new Product()) : Results.NotFound());

// 400 Bad Request
app.MapPost("/products", (Product p) => 
    string.IsNullOrEmpty(p.Name) 
        ? Results.BadRequest("Name is required") 
        : Results.Ok(p));

Dependency Injection

Injecting Services C#
// Register service
builder.Services.AddScoped<IProductService, ProductService>();

var app = builder.Build();

// Inject service into endpoint
app.MapGet("/products", async (IProductService service) =>
{
    var products = await service.GetAllAsync();
    return Results.Ok(products);
});

Route Groups

Route groups organize related endpoints and apply common configuration.

Using Route Groups C#
var products = app.MapGroup("/api/products");

products.MapGet("/", async (IProductService service) =>
    await service.GetAllAsync());

products.MapGet("/{id}", async (int id, IProductService service) =>
{
    var product = await service.GetByIdAsync(id);
    return product == null ? Results.NotFound() : Results.Ok(product);
});

products.MapPost("/", async (CreateProductDto dto, IProductService service) =>
{
    var product = await service.CreateAsync(dto);
    return Results.Created($"/api/products/{product.Id}", product);
});

products.MapPut("/{id}", async (int id, UpdateProductDto dto, IProductService service) =>
{
    await service.UpdateAsync(id, dto);
    return Results.NoContent();
});

products.MapDelete("/{id}", async (int id, IProductService service) =>
{
    await service.DeleteAsync(id);
    return Results.NoContent();
});

Group Configuration

Apply Metadata to Group C#
var products = app.MapGroup("/api/products")
    .RequireAuthorization()           // All endpoints require auth
    .WithTags("Products")             // OpenAPI tag
    .WithOpenApi();                   // Generate OpenAPI docs

Filters

Endpoint Filters C#
// Validation filter
app.MapPost("/products", (CreateProductDto dto) => Results.Ok(dto))
    .AddEndpointFilter(async (context, next) =>
    {
        var dto = context.GetArgument<CreateProductDto>(0);
        
        if (string.IsNullOrEmpty(dto.Name))
            return Results.BadRequest("Name is required");
        
        return await next(context);
    });

Complete InvenTrack Example

Program.cs C#
var builder = WebApplication.CreateBuilder(args);

// Register services
builder.Services.AddScoped<IProductService, ProductService>();

var app = builder.Build();

// Products API group
var products = app.MapGroup("/api/products")
    .WithTags("Products");

// GET /api/products
products.MapGet("/", async (IProductService service) =>
{
    var allProducts = await service.GetAllAsync();
    return Results.Ok(allProducts);
})
.WithName("GetProducts")
.Produces<List<Product>>(200);

// GET /api/products/{id}
products.MapGet("/{id}", async (int id, IProductService service) =>
{
    var product = await service.GetByIdAsync(id);
    return product == null 
        ? Results.NotFound() 
        : Results.Ok(product);
})
.WithName("GetProduct")
.Produces<Product>(200)
.Produces(404);

// POST /api/products
products.MapPost("/", async (CreateProductDto dto, IProductService service) =>
{
    var product = await service.CreateAsync(dto);
    return Results.CreatedAtRoute("GetProduct", new { id = product.Id }, product);
})
.Produces<Product>(201)
.Produces(400);

// PUT /api/products/{id}
products.MapPut("/{id}", async (int id, UpdateProductDto dto, IProductService service) =>
{
    var exists = await service.ExistsAsync(id);
    
    if (!exists)
        return Results.NotFound();
    
    await service.UpdateAsync(id, dto);
    return Results.NoContent();
})
.Produces(204)
.Produces(404);

// DELETE /api/products/{id}
products.MapDelete("/{id}", async (int id, IProductService service) =>
{
    var exists = await service.ExistsAsync(id);
    
    if (!exists)
        return Results.NotFound();
    
    await service.DeleteAsync(id);
    return Results.NoContent();
})
.Produces(204)
.Produces(404);

app.Run();

When to Use Minimal APIs

Use Minimal APIs Use Controllers
Simple APIs Complex APIs
Microservices Large monoliths
Prototypes Enterprise applications
Few endpoints Many endpoints
Performance-critical Feature-rich requirements

Best Practices

  • Route groups: Organize related endpoints
  • Dependency injection: Inject services into endpoints
  • Results helpers: Use Results.Ok(), Results.NotFound(), etc.
  • Metadata: Use WithName(), WithTags(), Produces()
  • Async: Use async/await for I/O operations
  • Validation: Add endpoint filters for validation
  • Organization: Consider extension methods for large APIs

Key Takeaways

  • Minimal APIs: Lightweight alternative to controllers
  • MapGet/Post/Put/Delete: Define HTTP endpoints
  • Results helpers: Return typed responses
  • Route groups: Organize and configure endpoints
  • Filters: Add cross-cutting concerns
  • DI: Inject services into endpoint handlers
  • Less code: Reduced boilerplate
  • Performance: Faster startup and execution
🎯 Next Steps

You now understand Minimal APIs! In the next section, we'll explore DTOs and AutoMapper—how to separate your API models from domain models and automatically map between them.