> ## Content Index
> Fetch the complete content index at: https://www.azurebrasil.cloud/llms.txt
> Use this file to discover other available public pages before exploring further.

# Da API simples à API profissional: organização e boas práticas no ASP.NET Core
- URL: https://www.azurebrasil.cloud/blog/da-api-simples-a-api-profissional-organizacao-e-boas-praticas-no-asp-net-core/
- Published: 2026-02-25T11:53:07.000Z
- Updated: 2026-02-25T11:53:07.000Z
- Author: Gabriel Fracalossi Nazario

Nos artigos anteriores, demos passos importantes na construção da nossa primeira API com [.NET](https://dotnet.microsoft.com/pt-br/learn/dotnet/what-is-dotnet?ref=azurebrasil.cloud). Começamos entendendo os fundamentos da plataforma e, em seguida, criamos uma API funcional utilizando [ASP.NET Core](https://dotnet.microsoft.com/pt-br/learn/aspnet/what-is-aspnet-core?ref=azurebrasil.cloud) e controllers tradicionais, explorando conceitos essenciais como rotas, verbos HTTP, [model binding](https://learn.microsoft.com/pt-br/aspnet/core/mvc/models/model-binding?view=aspnetcore-10.0&ref=azurebrasil.cloud) e estrutura do projeto.

Neste ponto da jornada, nossa API já funciona, responde requisições e expõe [endpoints](https://learn.microsoft.com/pt-br/security/zero-trust/integrate/endpoints?ref=azurebrasil.cloud) 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](https://learn.microsoft.com/pt-br/dotnet/core/extensions/dependency-injection/overview?ref=azurebrasil.cloud) 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](https://www.azurebrasil.cloud/blog/primeiros-passos-com-net/) e a [parte 2](https://www.azurebrasil.cloud/blog/criando-sua-primeira-api-com-net/)!

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:

![](https://storage.ghost.io/c/00/52/0052dced-0017-4d07-b190-1f5c48e0ab59/content/images/2026/02/image-1-1.png)

Caminho para criar novo arquivo "IProdutoService.cs"

![](https://storage.ghost.io/c/00/52/0052dced-0017-4d07-b190-1f5c48e0ab59/content/images/2026/02/image-2.png)

Interface de exemplo

```C#
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:

![](https://storage.ghost.io/c/00/52/0052dced-0017-4d07-b190-1f5c48e0ab59/content/images/2026/02/image-3-1.png)

Caminho para criar novo arquivo "ProdutoService.cs"

![](https://storage.ghost.io/c/00/52/0052dced-0017-4d07-b190-1f5c48e0ab59/content/images/2026/02/image-4.png)

Service de exemplo

```C#
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](https://learn.microsoft.com/pt-br/dotnet/framework/get-started/?ref=azurebrasil.cloud) fornece essas dependências para ela.

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

```C#
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:

![](https://storage.ghost.io/c/00/52/0052dced-0017-4d07-b190-1f5c48e0ab59/content/images/2026/02/image-5-1.png)

```C#
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**:

![](https://storage.ghost.io/c/00/52/0052dced-0017-4d07-b190-1f5c48e0ab59/content/images/2026/02/image-6-1.png)

```C#
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](https://www.azurebrasil.cloud/blog/entity-framework-em-apis-modernas/)
- 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](https://github.com/GabrielFracalossi/MinhaPrimeiraApi?ref=azurebrasil.cloud)!