ASP.NET Minimal APIs

As minimal APIs do .NET 6 permitem criar uma API completa com menos de 50 linhas de código. Neste artigo, vamos construir uma API de tarefas do zero, com banco de dados, validação e documentação, usando apenas o arquivo Program.cs como ponto central.

O que são minimal APIs

Uma das novidades do .NET 6 e do ASP.NET Core 6 é a chegada de um novo modelo de hospedagem e de um template enxuto. Ele substitui o padrão anterior e reduz bastante a quantidade de código necessária para expor endpoints HTTP.

Na prática, as minimal APIs eliminam a necessidade de controllers, da classe Startup e de boa parte da cerimônia do MVC. Em vez disso, os endpoints viram funções mapeadas diretamente em rotas.

Ao executar o comando dotnet new web com o .NET 6 instalado, o SDK cria um projeto com o mínimo de código possível. Dessa forma, o desenvolvedor parte de uma base limpa e adiciona apenas o que precisa.

Vale lembrar que você ainda pode usar dotnet new mvc ou dotnet new razor para criar templates mais robustos. Afinal, cada modelo atende necessidades diferentes, e o MVC continua sendo uma opção válida.

A documentação oficial de minimal APIs descreve o modelo como uma forma de criar APIs HTTP com dependências mínimas. Por isso, ele combina muito bem com microsserviços e aplicações pequenas.

Criando sua primeira minimal API

Para executar os códigos a seguir, você precisa do SDK do .NET 6 instalado e configurado. Com isso feito, basta executar o comando abaixo.

dotnet new web -o MiniTodo

O SDK cria uma nova API e, ao abri-la no Visual Studio ou no Visual Studio Code, você encontra apenas um arquivo relevante. Trata-se do Program.cs, com o conteúdo abaixo.

var builder = WebApplication.CreateBuilder(args);

var app = builder.Build();

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

app.Run();

Acredite, isso já é uma API funcional. Execute o comando dotnet run na pasta gerada e acesse o endereço exibido no terminal para ver a mensagem Hello World.

O WebApplication.CreateBuilder configura o host, o logging e a configuração com valores padrão. Em seguida, o MapGet associa a rota raiz a uma função anônima, e o Run inicia o servidor.

Modelando dados com records

O C# 9 introduziu um novo tipo para estruturas de dados, chamado record. Ele oferece igualdade por valor, imutabilidade e uma sintaxe muito mais curta que a de uma classe tradicional.

Esse tipo é perfeito para o nosso cenário, já que não teremos comportamento aqui, apenas dados. Então, vamos criar o record Todo em um arquivo Todo.cs, dentro de uma pasta chamada Models.

public record Todo(Guid Id, string Title, bool Done);

Repare que os records são bem menos burocráticos. Afinal, definimos todas as propriedades já na declaração, em uma única linha.

Tipando o retorno das minimal APIs

Um recurso que funciona muito bem no ASP.NET é o model binding, junto com a serialização automática. Nossa API recebe e retorna JSON, mas no C# trabalhamos com objetos, e o framework faz essa tradução nos dois sentidos.

Dessa forma, podemos simplesmente retornar um record ou qualquer outro objeto. Então, as minimal APIs convertem o objeto para JSON antes de enviá-lo a quem fez a requisição.

var builder = WebApplication.CreateBuilder(args);

var app = builder.Build();

app.MapGet("/", () => new Todo(Guid.NewGuid(), "Go to the gym", false));

app.Run();

Ao executar a API nesse momento, você verá um JSON com as propriedades e os valores informados no Todo. Por padrão, o serializador usa o padrão camelCase nos nomes das propriedades.

Retornando o HTTP status code correto

Outro item importante é o código HTTP que retornamos. Por exemplo, na criação de um item, o correto é retornar o status 201 Created.

Felizmente, o ASP.NET Core já traz tudo isso pronto na classe estática Results. Com ela, informamos o status de forma explícita e legível.

app.MapGet("/", () => Results.Ok(new Todo(Guid.NewGuid(), "Go to the gym", false)));

A classe Results oferece métodos como Ok, Created, NotFound, BadRequest e NoContent. Assim, cada endpoint comunica ao cliente exatamente o que aconteceu.

Integrando o Entity Framework às minimal APIs

Até aqui usamos poucas linhas, mas no mundo real trabalhamos com dados. Então, como fica o Entity Framework Core nas minimal APIs?

O primeiro passo é adicionar os pacotes necessários para o EF funcionar. Neste exemplo, usamos o SQLite como banco de dados, porque ele dispensa qualquer instalação de servidor.

dotnet add package Microsoft.EntityFrameworkCore.Sqlite
dotnet add package Microsoft.EntityFrameworkCore.Design

Em seguida, vamos criar o nosso contexto de dados, que representa o banco de dados dentro da aplicação. Para organizar melhor, a classe fica na pasta Data.

using Microsoft.EntityFrameworkCore;

namespace MiniTodo.Data
{
    public class AppDbContext : DbContext
    {
        public DbSet<Todo> Todos { get; set; }

        // Usa um arquivo SQLite local chamado app.db
        protected override void OnConfiguring(DbContextOptionsBuilder options)
            => options.UseSqlite("DataSource=app.db;Cache=Shared");
    }
}

Não há nada diferente aqui em relação a outros projetos com EF Core. Na verdade, é o mesmo tipo de arquivo usado em APIs com controllers.

Se você ainda está decidindo entre ferramentas de acesso a dados, o artigo Dapper ou Entity Framework Core: qual escolher? compara as duas opções. Ele ajuda a entender quando cada uma faz mais sentido.

Gerando o banco com migrations

As migrations também funcionam perfeitamente nesse modelo. Como você pode notar, ainda não existe um banco chamado app.db no projeto.

Por isso, executamos os comandos abaixo para criar a primeira migração e gerar o banco de dados.

dotnet ef migrations add InitialCreation
dotnet ef database update

Para essa etapa, você precisa das ferramentas do EF instaladas. Caso ainda não tenha feito isso, execute dotnet tool install --global dotnet-ef antes das migrações.

Pronto, agora temos o contexto de dados e o banco gerado. Além disso, o EF Core consegue mapear o record Todo usando o construtor com os parâmetros correspondentes às propriedades.

Injeção de dependência nas minimal APIs

Outro recurso do ASP.NET Core que continua presente é a injeção de dependência. Assim como nas APIs com MVC, registramos o DbContext no container de serviços.

using MiniTodo.Data;

var builder = WebApplication.CreateBuilder(args);

// Registra o contexto no container de DI
builder.Services.AddDbContext<AppDbContext>();

var app = builder.Build();

A partir daí, qualquer endpoint pode pedir o AppDbContext como parâmetro. Então, o framework resolve a instância automaticamente a cada requisição.

Criando o endpoint GET

Com o contexto pronto, vamos usá-lo no método GET da API. Para isso, basta declarar o AppDbContext como parâmetro da função.

app.MapGet("/v1/todos", (AppDbContext context) =>
{
    var todos = context.Todos;
    return todos is not null ? Results.Ok(todos) : Results.NotFound();
});

No retorno, usamos o pattern matching do C#, comparando todos is not null em vez de todos != null. Essa comparação permite retornar Results.NotFound() com status 404 ou Results.Ok() com status 200.

Como o DbSet nunca é nulo, uma variação comum é materializar a lista com ToList e devolver sempre 200. Ainda assim, o exemplo mostra bem como combinar o pattern matching com os resultados HTTP.

Repare que o framework distingue a origem de cada parâmetro sozinho. Ou seja, tipos registrados no container vêm da injeção, valores simples vêm da rota ou da query string e objetos complexos vêm do corpo da requisição.

View models e validações

E as validações, os pacotes externos e as view models? Sim, dá para usar tudo isso nas minimal APIs, e vamos começar instalando o Flunt.

dotnet add package Flunt

O Flunt é uma biblioteca de notificações que permite acumular erros de validação em vez de lançar exceções. Dessa forma, devolvemos todos os problemas de uma só vez ao cliente.

Em seguida, vamos criar a view model CreateTodoViewModel na pasta ViewModels, para manter o projeto organizado.

using Flunt.Notifications;
using Flunt.Validations;

namespace MiniTodo.ViewModels
{
    public class CreateTodoViewModel : Notifiable<Notification>
    {
        public string Title { get; set; }

        public Todo MapTo()
        {
            // Acumula as notificações de validação
            AddNotifications(new Contract<Notification>()
                .Requires()
                .IsNotNull(Title, "Title", "Task title is required")
                .IsGreaterThan(Title, 5, "Title", "Title must have more than 5 characters"));

            return new Todo(Guid.NewGuid(), Title, false);
        }
    }
}

Para facilitar, criamos o método MapTo, que valida os dados e retorna o Todo. Com isso, o endpoint POST fica assim.

app.MapPost("/v1/todos", (AppDbContext context, CreateTodoViewModel model) =>
{
    var todo = model.MapTo();
    if (!model.IsValid)
        return Results.BadRequest(model.Notifications);

    context.Todos.Add(todo);
    context.SaveChanges();

    return Results.Created($"/v1/todos/{todo.Id}", todo);
});

Se a validação falhar, o cliente recebe o status 400 com a lista de notificações. Caso contrário, o EF grava a tarefa e a API devolve 201 com o endereço do novo recurso.

Completando o CRUD nas minimal APIs

Com GET e POST prontos, falta pouco para um CRUD completo. Por isso, vamos adicionar a busca por identificador, a conclusão de uma tarefa e a exclusão.

Buscando uma tarefa pelo identificador

Parâmetros de rota ficam entre chaves no padrão da URL. Em seguida, o framework converte o valor para o tipo declarado na função, neste caso um Guid.

app.MapGet("/v1/todos/{id}", (Guid id, AppDbContext context) =>
{
    var todo = context.Todos.Find(id);

    // Retorna 404 quando a tarefa não existe
    return todo is not null ? Results.Ok(todo) : Results.NotFound();
});

Se o valor da rota não puder ser convertido em Guid, a requisição falha antes de chegar ao código. Dessa forma, não precisamos validar o formato do identificador manualmente.

Atualizando e removendo tarefas

Como o Todo é um record imutável, usamos a expressão with para gerar uma cópia com a propriedade alterada. Depois, pedimos ao EF Core que aplique os novos valores à entidade rastreada.

app.MapPut("/v1/todos/{id}/done", (Guid id, AppDbContext context) =>
{
    var todo = context.Todos.Find(id);
    if (todo is null)
        return Results.NotFound();

    // Cria uma cópia do record com Done = true
    var updated = todo with { Done = true };
    context.Entry(todo).CurrentValues.SetValues(updated);
    context.SaveChanges();

    return Results.Ok(updated);
});

app.MapDelete("/v1/todos/{id}", (Guid id, AppDbContext context) =>
{
    var todo = context.Todos.Find(id);
    if (todo is null)
        return Results.NotFound();

    context.Todos.Remove(todo);
    context.SaveChanges();

    return Results.NoContent();
});

Os métodos MapPut e MapDelete seguem exatamente a mesma lógica do MapGet e do MapPost. Assim, cada verbo HTTP ganha o próprio método de mapeamento.

Repare que a exclusão devolve 204 No Content, o status recomendado quando a operação não tem corpo de resposta. Portanto, o cliente sabe que deu certo sem receber dados extras.

Usando métodos assíncronos

Os handlers das minimal APIs também aceitam funções assíncronas. Basta marcar a lambda com async e usar os métodos assíncronos do EF Core.

app.MapGet("/v1/todos", async (AppDbContext context) =>
{
    // Libera a thread enquanto o banco responde
    var todos = await context.Todos.AsNoTracking().ToListAsync();
    return Results.Ok(todos);
});

Em APIs com muitas requisições simultâneas, essa abordagem melhora o aproveitamento das threads do servidor. Além disso, o AsNoTracking evita o custo do rastreamento em consultas somente leitura.

Documentando minimal APIs com Swagger

Para finalizar a API, vamos adicionar suporte ao Swagger, que gera a documentação no padrão OpenAPI. O primeiro passo é instalar o pacote Swashbuckle.

dotnet add package Swashbuckle.AspNetCore

Com o pacote instalado, registramos os serviços do Swagger e ativamos a interface gráfica. Repare no AddEndpointsApiExplorer, que expõe os metadados dos endpoints das minimal APIs.

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDbContext<AppDbContext>();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var app = builder.Build();

// Gera o documento OpenAPI e a interface visual
app.UseSwagger();
app.UseSwaggerUI();

Agora basta executar a API e navegar até /swagger/index.html, na porta exibida pelo terminal. Então, você terá a API documentada e pronta para testes no navegador.

Tipagem no Swagger

Como retornamos Results.Ok, o Swagger não consegue descobrir sozinho o tipo do retorno. Consequentemente, ele não gera os schemas das respostas.

Para resolver isso, usamos o método de extensão Produces, que informa o tipo e, opcionalmente, o status code.

app.MapGet("/v1/todos", (AppDbContext context) =>
{
    var todos = context.Todos;
    return todos is not null ? Results.Ok(todos) : Results.NotFound();
}).Produces<Todo>();

Agora temos suporte à documentação com schemas. Além disso, você pode encadear várias chamadas, como Produces(StatusCodes.Status404NotFound), para documentar cada resposta possível.

Outro método útil é o WithName, que dá um nome ao endpoint. Segundo a documentação, esse nome também vira o identificador da operação no OpenAPI.

Organizando o Program.cs

Conforme a API cresce, manter todos os endpoints no Program.cs deixa o arquivo longo e difícil de navegar. Uma solução simples é mover os mapeamentos para métodos de extensão.

public static class TodoEndpoints
{
    // Agrupa todos os endpoints de tarefas em um único lugar
    public static WebApplication MapTodoEndpoints(this WebApplication app)
    {
        app.MapGet("/v1/todos", (AppDbContext context)
            => Results.Ok(context.Todos.ToList()));

        app.MapGet("/v1/todos/{id}", (Guid id, AppDbContext context)
            => context.Todos.Find(id) is Todo todo
                ? Results.Ok(todo)
                : Results.NotFound());

        return app;
    }
}

Com isso, o Program.cs volta a ter poucas linhas e apenas chama app.MapTodoEndpoints(). Além disso, cada domínio da aplicação ganha o próprio arquivo de endpoints.

Essa organização também facilita a leitura em equipe. Afinal, quem procura uma rota de tarefas sabe exatamente onde encontrá-la.

Outra opção é extrair os handlers para métodos estáticos nomeados, em vez de lambdas. Dessa forma, você consegue testá-los isoladamente, sem subir o servidor.

Quando usar minimal APIs

As minimal APIs brilham em microsserviços, protótipos e APIs com poucos endpoints. Nesses casos, a ausência de controllers reduz o código e acelera o início do projeto.

Elas também têm um desempenho excelente, porque o pipeline de execução de cada endpoint é mais curto que o do MVC. Por consequência, aplicações com alto volume de requisições se beneficiam desse modelo.

Além disso, o modelo reduz a barreira de entrada para quem está começando no ASP.NET Core. Afinal, o iniciante enxerga a requisição, a rota e a resposta em poucas linhas.

Por outro lado, projetos muito grandes exigem disciplina de organização. Por exemplo, você pode agrupar os mapeamentos em métodos de extensão, separados por domínio, para evitar um Program.cs gigante.

Também é importante saber que o .NET 6 ainda não oferece, nesse modelo, alguns recursos do MVC. Filtros de action e validação automática com data annotations são exemplos, por isso usamos o Flunt aqui.

Para ver o modelo aplicado em um projeto completo, o artigo Criando um CRUD com Blazor WASM e Minimal APIs mostra a integração com um front-end. Assim, você enxerga o fluxo de ponta a ponta.

Conclusão

Podemos criar APIs simples e rápidas com as minimal APIs do ASP.NET Core 6 e, ainda assim, usar todos os recursos da plataforma. Injeção de dependência, Entity Framework, migrations, validações e Swagger funcionam normalmente.

O resultado é uma API completa em poucas linhas, fácil de ler e de manter. Portanto, vale considerar esse modelo sempre que o projeto não exigir toda a estrutura do MVC.

O código-fonte completo deste exemplo está disponível no GitHub. Dessa forma, você pode clonar o repositório e evoluir a API com novos endpoints.

FAQ

O que são minimal APIs?

São um modelo do ASP.NET Core, lançado no .NET 6, para criar APIs HTTP com o mínimo de código. Os endpoints são funções mapeadas diretamente em rotas, sem controllers.

Minimal APIs substituem o MVC?

Não, os dois modelos convivem no ASP.NET Core. O MVC continua indicado para projetos que precisam de filtros, convenções e toda a estrutura de controllers.

Dá para usar Entity Framework com minimal APIs?

Sim, basta registrar o DbContext com AddDbContext e recebê-lo como parâmetro do endpoint. As migrations funcionam da mesma forma que em outros projetos.

Como documentar minimal APIs com Swagger?

Instale o pacote Swashbuckle.AspNetCore, registre AddEndpointsApiExplorer e AddSwaggerGen e ative UseSwagger e UseSwaggerUI. Use Produces para informar os tipos de retorno.

Como validar dados em minimal APIs no .NET 6?

O .NET 6 não valida data annotations automaticamente nesse modelo. Por isso, use uma biblioteca como o Flunt ou o FluentValidation e retorne Results.BadRequest com os erros.