Da API simples à API profissional: organização e boas práticas no ASP.NET Core

Da API simples à API profissional: organização e boas práticas no ASP.NET Core

25 de Fevereiro de 2026

Nos artigos anteriores, demos passos importantes na construção da nossa primeira API com .NET. Começamos entendendo os fundamentos da plataforma e, em seguida, criamos uma API funcional utilizando ASP.NET Core e controllers tradicionais, explorando conceitos essenciais como rotas, verbos HTTP, model binding e estrutura do projeto.

Neste ponto da jornada, nossa API já funciona, responde requisições e expõe endpoints reais. Porém, se você observar o código atual com um olhar mais crítico e profissional, perceberá algo muito comum no início da carreira: toda a lógica da aplicação está concentrada dentro dos controllers.

Isso não é um erro. Inclusive, faz parte do processo de aprendizado. Mas, para que o projeto possa crescer de forma natural, coerente e à prova de erros, precisamos começar a organizar melhor as responsabilidades do código.

Neste artigo, vamos dar exatamente esse próximo passo: evoluir a API de um exemplo simples para uma estrutura mais profissional, introduzindo a camada de serviços (services), aprofundando o conceito de separação de responsabilidades e utilizando corretamente a injeção de dependência do ASP.NET Core.

Recomendo a leitura dos outros artigos antes de dar sequência aqui, pois esse é uma continuação direta do mesmo projeto. Leia a parte 1 e a parte 2!

Relembrando: até aqui, nossa ProdutosController:

  • Define rotas
  • Recebe requisições HTTP
  • Manipula dados
  • Contém regras de negócio
  • Decide o que será retornado ao cliente

Em projetos pequenos, isso até parece aceitável. Inclusive, muitos tutoriais param exatamente aqui. O problema começa quando a aplicação cresce.

Imagine que agora precisamos:

  • Validar regras mais complexas
  • Aplicar descontos
  • Verificar estoque
  • Registrar logs
  • Integrar com outro sistema

Se toda essa lógica estiver dentro do controller, ele começará a crescer de forma descontrolada. Em cenários assim, controllers deixam de ser simples “pontos de entrada HTTP” e passam a concentrar decisões de negócio, validações e manipulação de dados.

Isso gera alguns efeitos colaterais perigosos:

  • Controllers ficam grandes e difíceis de manter
  • Regras de negócio se espalham
  • Código fica repetitivo
  • Testar o código se torna mais complexo
  • Qualquer mudança gera impacto em vários pontos
  • Maior chance de bugs

É nesse momento que entra um dos conceitos mais importantes do desenvolvimento profissional: separação de responsabilidades.

A ideia central é simples, mas poderosa. O controller não deve conter regras de negócio. Como já vimos nos artigos anteriores, o papel do controller é:

  • Receber a requisição
  • Validar entrada básica
  • Delegar a execução para outra camada
  • Traduzir o resultado para uma resposta HTTP

Quem deve decidir como as coisas funcionam é uma camada específica para isso: o service.

Essa separação cria um limite claro entre:

  • Camada de apresentação (HTTP)
  • Camada de regra de negócio

Esse pequeno ajuste estrutural muda completamente o nível de maturidade do projeto.

Vamos aplicar isso na prática. Na raiz do projeto, crie uma nova pasta chamada Services. Dentro dela, vamos criar dois arquivos:

  • IProdutoService.cs
  • ProdutoService.cs

Começamos pela interface, que define o contrato do serviço:

Caminho para criar novo arquivo "IProdutoService.cs"

Interface de exemplo

using MinhaPrimeiraApi.Models;

namespace MinhaPrimeiraApi.Services
{
  public interface IProdutoService
  {
    IEnumerable<Produto> ObterTodos();
    Produto? ObterPorId(int id);
    void Adicionar(Produto produto);
  }
}

Código disponível para copiar

Aqui estamos deixando claro o que esse serviço é capaz de fazer, sem nos preocupar ainda com "como" isso será implementado. Perceba que a interface não sabe:

  • Se os dados vêm de uma lista
  • Se vêm de um banco
  • Se vêm de uma API externa

Ela apenas define o comportamento esperado. Esse é um dos primeiros passos para escrever um código flexível.

Agora, vamos criar a classe que implementa essa interface:

Caminho para criar novo arquivo "ProdutoService.cs"

Service de exemplo

using MinhaPrimeiraApi.Models;

namespace MinhaPrimeiraApi.Services
{
    public class ProdutoService : IProdutoService
    {
        private static readonly List<Produto> Produtos = new()
        {
          new Produto { Id = 1, Nome = "Teclado", Preco = 150 },
          new Produto { Id = 2, Nome = "Mouse", Preco = 80 }
        };

        public IEnumerable<Produto> ObterTodos()
        {
            return Produtos;
        }

        public Produto? ObterPorId(int id)
        {
            return Produtos.FirstOrDefault(p => p.Id == id);
        }

        public void Adicionar(Produto produto)
        {
            Produtos.Add(produto);
        }
    }
}

Código disponível para copiar

Aqui ainda estamos utilizando uma lista em memória, mas agora existe uma diferença fundamental: a lista não pertence mais ao controller.

Note que:

  • Toda a lógica relacionada a produtos está concentrada em um único lugar
  • Se no futuro trocarmos essa lista por um repositório com banco de dados, o controller não precisará ser alterado

Esse é o verdadeiro poder da abstração.

O ASP.NET Core possui um sistema de injeção de dependência nativo, simples e poderoso. Injeção de dependência significa que, ao invés de uma classe criar suas próprias dependências, o framework fornece essas dependências para ela.

Sem DI (dependence injection), o controller poderia fazer algo como:

var service = new ProdutoService();

Isso cria um forte acoplamento, que limita nossa aplicação.

Com DI, fazemos o registro no Program.cs. Abra o arquivo e adicione o registro do serviço:

using MinhaPrimeiraApi.Services;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container (traditional controllers).
builder.Services.AddControllers();
builder.Services.AddScoped<IProdutoService, ProdutoService>();

// Add OpenAPI (Swagger).
builder.Services.AddOpenApi();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.MapOpenApi();
}

app.UseHttpsRedirection();

// Map Controllers.
app.MapControllers();

app.Run();

Código disponível para copiar

O método AddScoped() define o tempo de vida do serviço.

No caso do Scoped:

  • Uma instância é criada por requisição HTTP
  • A mesma instância é reutilizada durante toda aquela requisição

Existem outros ciclos de vida como Transient e Singleton, e entender isso se torna extremamente importante em aplicações maiores.

Aqui estamos dizendo ao .NET:

Sempre que alguém precisar de um IProdutoService, forneça uma instância de ProdutoService.

Esse padrão é amplamente utilizado em aplicações reais e é um dos pilares da arquitetura moderna no .NET.

Agora vamos ajustar o controller para que ele não saiba mais como os dados são manipulados. Em vez de criar manualmente uma instância do serviço dentro da classe, vamos recebê-lo por meio do construtor.

Mas antes, vale entender melhor o papel do construtor.

O construtor é um método especial de uma classe que é executado automaticamente no momento em que uma nova instância dessa classe é criada. No contexto do ASP.NET Core, quem cria a instância do controller não é você diretamente, mas sim o próprio framework, a cada requisição HTTP.

Isso significa que, quando uma requisição chega para o endpoint ProdutosController, o ASP.NET Core precisa:

  • Criar uma instância do controller
  • Resolver todas as dependências necessárias
  • Só então executar o método correspondente (GET, POST, etc.)

Quando declaramos uma dependência como parâmetro do construtor, estamos explicitando quais recursos aquela classe precisa para funcionar corretamente. No nosso caso:

Para que este controller funcione, ele precisa de algo que implemente IProdutoService.

Como já registramos essa interface no container de injeção de dependência com AddScoped(), o ASP.NET Core consegue automaticamente:

  • Localizar a implementação registrada (ProdutoService)
  • Criar a instância respeitando o ciclo de vida configurado
  • Injetá-la no construtor do controller
  • Entregar o controller já pronto para uso

Perceba a diferença conceitual: o controller não cria suas dependências, ele apenas declara o que precisa. Essa inversão de responsabilidade reduz acoplamento, melhora a testabilidade e torna o código mais previsível.

Esse padrão é chamado de Injeção de Dependência via Construtor (Constructor Injection) e é considerado a forma mais recomendada de trabalhar com dependências no .NET moderno.

Agora que entendemos isso, podemos ajustar o controller para receber o serviço via construtor. Atualize o ProdutosController.cs:

using Microsoft.AspNetCore.Mvc;
using MinhaPrimeiraApi.Models;
using MinhaPrimeiraApi.Services;

namespace MinhaPrimeiraApi.Controllers
{
    [ApiController]
    [Route("api/[controller]")]
    public class ProdutosController(IProdutoService produtoService) : ControllerBase
    {
        [HttpGet]
        public IActionResult Get()
        {
            var produtos = produtoService.ObterTodos();
            return Ok(produtos);
        }

        [HttpGet("{id}")]
        public IActionResult GetById(int id)
        {
            var produto = produtoService.ObterPorId(id);
            if (produto == null)
            {
                return NotFound();
            }

            return Ok(produto);
        }

        [HttpPost]
        public IActionResult Post(Produto produto)
        {
            produtoService.Adicionar(produto);
            return CreatedAtAction(nameof(GetById), new { id = produto.Id }, produto);
        }
    }
}

Código disponível para copiar

Observe como o controller ficou, agora ele:

  • Não conhece a implementação concreta
  • Não manipula lista diretamente
  • Não toma decisões de armazenamento
  • Foca apenas em HTTP
  • Está totalmente desacoplado da lógica de negócio

Ele apenas delega. Isso reduz drasticamente o acoplamento e aumenta a previsibilidade do sistema. Esse é exatamente o tipo de código que você encontrará em projetos profissionais.

Essa separação traz benefícios claros:

  • Código mais organizado e legível
  • Facilidade para testes
  • Menor acoplamento
  • Evolução mais segura do projeto
  • Redução de efeitos colaterais
  • Melhor distribuição de responsabilidades
  • Base sólida para banco de dados, validações e regras mais complexas

Mesmo que, neste momento, tudo ainda seja simples, a estrutura já está pronta para crescer.

Nossa API ainda utiliza dados em memória, mas agora ela está arquiteturalmente preparada para:

  • Integrar com um banco de dados
  • Utilizar o Entity Framework Core
  • Implementar validações mais robustas
  • Tratar erros de forma padronizada
  • Evoluir sem refatorações dolorosas

Esse é o tipo de evolução que acontece em projetos reais: passo a passo, com responsabilidade.

Neste artigo, transformamos uma API simples em uma base muito mais profissional e demos um dos passos mais importantes da série até agora. Não adicionamos novas funcionalidades visíveis ao usuário, mas elevamos significativamente a qualidade estrutural da aplicação.

Introduzimos a camada de serviços, aplicamos injeção de dependência na prática e organizamos melhor as responsabilidades do projeto. Mais do que adicionar código, o objetivo aqui foi mudar a forma de pensar a aplicação. A partir de agora, cada nova funcionalidade pode ser adicionada com muito mais segurança e clareza.

No próximo artigo da série, vamos dar mais um passo importante nessa evolução, integrando a API com um banco de dados e introduzindo o Entity Framework Core, aproximando ainda mais o projeto de um cenário real de produção. A ideia continua a mesma: evoluir a API de forma gradual, conectando teoria e prática, para que cada conceito faça sentido dentro de um projeto real.

Disponibilizei o projeto de exemplo que usamos aqui no artigo. Você pode baixar direto do GitHub clicando aqui!

Confira mais:

Fique por dentro das novidades

Assine nossa newsletter e receba as últimas atualizações e artigos diretamente em seu email.

Assinar gratuitamente