Pular para o conteúdo

Layered Architecture (Arquitetura em Camadas): entendendo um dos Design Patterns mais usados no back-end

Se você está começando no desenvolvimento back-end ou já escreve código há algum tempo, provavelmente já se perguntou: “Onde eu coloco essa regra de negócio?”, “Esse código deveria estar aqui ou em outro arquivo?” ou até “Por que esse projeto virou uma bagunça tão rápido?”. É exatamente nesse ponto que a Layered Architecture, ou Arquitetura em Camadas, entra em cena.

A arquitetura em camadas é um dos design patterns mais antigos e mais utilizados no desenvolvimento de software. Ela não é complexa, não exige frameworks específicos e, ainda assim, resolve um problema enorme: organização de código e separação de responsabilidades. Em vez de misturar tudo em um único lugar, a aplicação é dividida em camadas bem definidas, cada uma com um papel claro.

De forma simples, a Layered Architecture costuma separar o sistema em camadas como Controller, Service, Domain e Repository. Cada camada conversa apenas com a camada “vizinha”, o que deixa o código mais fácil de entender, testar, manter e evoluir. Frameworks modernos como o NestJS já nascem praticamente prontos para esse padrão, o que torna o aprendizado ainda mais natural para quem está começando.

Neste artigo, vamos conversar sobre o que é a arquitetura em camadas, por que ela é tão popular, quais problemas ela resolve e como aplicá-la na prática usando NestJS, sempre com exemplos simples e linguagem direta. A ideia aqui não é complicar, mas te ajudar a enxergar arquitetura como algo acessível e essencial no dia a dia de qualquer dev.

O que é Layered Architecture na prática?

Na prática, Layered Architecture é uma forma de organizar o código separando a aplicação em camadas, onde cada camada tem uma responsabilidade bem definida. A ideia principal é simples: cada parte do sistema faz apenas o que foi projetada para fazer, sem invadir o papel das outras.

Em vez de ter um arquivo gigante que busca dados no banco, aplica regras de negócio, valida informações e ainda responde a requisição HTTP, a arquitetura em camadas divide isso em partes menores e mais organizadas. Isso deixa o código mais fácil de entender, testar e manter.

De forma clássica, uma aplicação em camadas costuma ser organizada assim:

  • Controller (Camada de Apresentação)
  • Service (Camada de Aplicação ou Negócio)
  • Domain (Camada de Domínio)
  • Repository (Camada de Acesso a Dados)

Nem todo projeto precisa ter exatamente essas quatro camadas, mas o conceito central é sempre o mesmo: separar responsabilidades.

Como as camadas se comunicam?

Um ponto muito importante da Layered Architecture é que as camadas não se comunicam de qualquer jeito. Existe um fluxo bem definido:

  • O Controller recebe a requisição (HTTP, por exemplo)
  • O Controller chama o Service
  • O Service aplica as regras de negócio
  • O Service usa o Repository para acessar dados
  • O resultado sobe o caminho inverso até chegar ao usuário

Ou seja, o Controller não acessa o banco diretamente, e o Repository não sabe nada sobre HTTP ou requisições. Cada camada conhece apenas o necessário para cumprir sua função.

Por que isso é considerado um Design Pattern?

A Layered Architecture é considerada um design pattern arquitetural porque ela resolve um problema recorrente no desenvolvimento de software: o acoplamento excessivo. Quando tudo está misturado, qualquer mudança simples vira um risco enorme. Com camadas bem definidas, você consegue evoluir partes do sistema sem quebrar tudo.

É exatamente por isso que frameworks como o NestJS adotam esse padrão quase como regra. Quando você cria um controller, um service e um module no NestJS, você já está, mesmo sem perceber, aplicando arquitetura em camadas.

Camada Controller: o ponto de entrada da aplicação

A camada Controller é a porta de entrada da sua aplicação. É ela que recebe as requisições externas, geralmente via HTTP, e devolve uma resposta. Em termos simples: o Controller conversa com o mundo externo, mas não resolve problemas complexos sozinho.

Uma regra de ouro da Layered Architecture é:
Controller não contém regra de negócio.
Ele apenas orquestra a chamada para a camada correta.

No dia a dia, isso significa que o Controller deve:

  • Receber dados da requisição (body, params, query)
  • Validar informações básicas (ou delegar validações)
  • Chamar o Service responsável
  • Retornar a resposta no formato correto

Se você começa a escrever lógica complexa dentro do Controller, é um sinal claro de que algo está errado na arquitetura.

Controller no NestJS

O NestJS deixa essa separação extremamente clara. Quando você cria um controller, ele já nasce com um papel bem definido.

Exemplo simples de um Controller no NestJS:

import { Controller, Get, Post, Body } from '@nestjs/common';
import { UsersService } from './users.service';

@Controller('users')
export class UsersController {
  constructor(private readonly usersService: UsersService) {}

  @Post()
  create(@Body() data: { name: string; email: string }) {
    return this.usersService.createUser(data);
  }

  @Get()
  findAll() {
    return this.usersService.listUsers();
  }
}

Perceba alguns pontos importantes:

  • O Controller não sabe como o usuário é salvo
  • Ele não acessa banco de dados
  • Ele apenas chama métodos do UsersService

Isso deixa o código limpo e fácil de entender. Se amanhã a regra para criar usuário mudar, você mexe no Service, não no Controller.

Erros comuns em Controllers

Quem está começando costuma cometer alguns erros bem comuns, como:

  • Acessar diretamente o banco de dados
  • Escrever regras de negócio complexas
  • Misturar lógica de autenticação, validação e persistência

Esses erros funcionam no início, mas rapidamente tornam o projeto difícil de manter. A arquitetura em camadas evita exatamente esse tipo de problema.

Camada Service: onde vive a regra de negócio

Se o Controller é apenas o ponto de entrada, a camada Service é o cérebro da aplicação. É aqui que ficam as regras de negócio, decisões importantes e fluxos que dão sentido ao sistema. Em uma Layered Architecture bem feita, quase toda a inteligência do software está concentrada nessa camada.

De forma simples, o Service responde perguntas como:

  • Posso criar esse usuário?
  • Esse pedido é válido?
  • O usuário tem permissão para executar essa ação?
  • O que acontece depois que um registro é salvo?

O Service não se preocupa com HTTP, nem com detalhes de banco de dados. Ele apenas executa regras e coordena chamadas para outras partes do sistema.

Service no NestJS

No NestJS, o Service é criado com o decorator @Injectable() e normalmente é injetado no Controller. Isso já incentiva automaticamente a separação de responsabilidades.

Exemplo simples de um Service:

import { Injectable } from '@nestjs/common';
import { UsersRepository } from './users.repository';

@Injectable()
export class UsersService {
  constructor(private readonly usersRepository: UsersRepository) {}

  async createUser(data: { name: string; email: string }) {
    const userExists = await this.usersRepository.findByEmail(data.email);

    if (userExists) {
      throw new Error('Usuário já existe');
    }

    return this.usersRepository.create(data);
  }

  async listUsers() {
    return this.usersRepository.findAll();
  }
}

Veja como o Service:

  • Aplica uma regra de negócio clara (não permitir e-mail duplicado)
  • Não sabe como o banco funciona internamente
  • Depende de um Repository para acessar dados

Isso torna o código testável, reutilizável e fácil de evoluir. Se amanhã você mudar o banco de dados, a regra de negócio continua intacta.

Por que o Service não deve acessar HTTP nem banco diretamente?

Misturar responsabilidades é um dos maiores erros em projetos iniciantes. Quando o Service começa a lidar com Request, Response ou queries SQL, o acoplamento aumenta muito.

O papel do Service é pensar o negócio, não o transporte nem a persistência. Essa separação deixa a aplicação mais organizada e profissional, algo essencial em projetos reais e entrevistas técnicas.

Camada Repository: isolando o acesso a dados

A camada Repository é a responsável por tudo que envolve acesso a dados. Banco de dados, ORM, queries SQL, chamadas para APIs externas ou qualquer outra forma de persistência devem ficar aqui. A grande ideia é simples: o restante da aplicação não precisa saber de onde os dados vêm.

Quando você usa Layered Architecture corretamente, o Service não faz ideia se os dados estão em um banco SQL, NoSQL, em memória ou vindo de uma API externa. Ele apenas chama métodos do Repository e recebe os dados de volta.

O papel do Repository na arquitetura em camadas

O Repository funciona como um contrato entre o domínio da aplicação e a infraestrutura. Ele encapsula detalhes técnicos e protege o resto do sistema contra mudanças.

Responsabilidades típicas do Repository:

  • Buscar dados no banco
  • Salvar, atualizar e remover registros
  • Traduzir dados do formato do banco para objetos da aplicação
  • Executar queries específicas

O que ele não deve fazer:

  • Aplicar regra de negócio
  • Tomar decisões complexas
  • Conhecer detalhes de HTTP ou Controllers

Repository no NestJS

O NestJS não obriga o uso de repositories explícitos, mas é uma prática extremamente comum, principalmente quando usamos TypeORM, Prisma ou Sequelize.

Exemplo simples de um Repository manual:

import { Injectable } from '@nestjs/common';

@Injectable()
export class UsersRepository {
  private users = [];

  async findByEmail(email: string) {
    return this.users.find(user => user.email === email);
  }

  async findAll() {
    return this.users;
  }

  async create(data: { name: string; email: string }) {
    const newUser = { id: Date.now(), ...data };
    this.users.push(newUser);
    return newUser;
  }
}

Mesmo sendo um exemplo em memória, o conceito é o mesmo. Se amanhã você decidir trocar isso por um banco real usando Prisma, o Service praticamente não muda.

Benefícios reais do uso de Repository

Separar o acesso a dados traz vantagens enormes:

  • Facilita testes unitários (você pode mockar o Repository)
  • Reduz o acoplamento com o banco
  • Torna a aplicação mais flexível e escalável

Esse padrão é muito valorizado em times profissionais e aparece com frequência em entrevistas técnicas.

Camada de Domínio: quando e por que ela existe

A Camada de Domínio é onde ficam os conceitos centrais do negócio da aplicação. Nem todo projeto precisa de uma camada de domínio explícita, mas quando o sistema começa a crescer, ela se torna extremamente valiosa.

Diferente do Service, que orquestra regras e fluxos, o Domínio representa o que o sistema é, não o que ele faz. Aqui moram entidades, regras fundamentais e comportamentos que fazem parte do coração do negócio.

Em projetos simples, o domínio pode ser apenas um conjunto de interfaces ou classes básicas. Em sistemas mais complexos, ele se aproxima bastante de conceitos do Domain-Driven Design (DDD).

O que normalmente fica na Camada de Domínio?

Alguns exemplos comuns:

  • Entidades (User, Order, Product)
  • Value Objects (Email, CPF, Money)
  • Interfaces de contratos (ex: UserRepository)
  • Regras que não dependem de infraestrutura

A ideia é que o domínio seja o mais independente possível de frameworks. Ele não deveria conhecer NestJS, TypeORM ou qualquer detalhe técnico.

Exemplo simples de Domínio

Um exemplo básico de entidade no domínio:

export class User {
  constructor(
    public readonly id: number,
    public name: string,
    public email: string,
  ) {}

  changeEmail(newEmail: string) {
    if (!newEmail.includes('@')) {
      throw new Error('E-mail inválido');
    }

    this.email = newEmail;
  }
}

Note que:

  • A entidade não sabe nada sobre banco ou HTTP
  • A regra de validação faz parte do próprio domínio
  • O código pode ser reutilizado em qualquer contexto

Domain no NestJS: precisa sempre?

Não. E isso é importante deixar claro.

Em projetos pequenos, adicionar uma camada de domínio muito elaborada pode ser exagero. NestJS já funciona muito bem com Controller + Service + Repository. A camada de domínio começa a fazer sentido quando:

  • As regras de negócio ficam mais complexas
  • O sistema cresce
  • Você quer proteger o core da aplicação de mudanças técnicas

Arquitetura não é sobre seguir regras rígidas, mas sobre fazer boas escolhas de acordo com o contexto.

Fluxo completo de uma requisição na Layered Architecture

Até aqui falamos de cada camada separadamente. Agora é hora de ver como tudo isso funciona junto, no mundo real, quando uma requisição chega na sua aplicação.

Imagine uma requisição simples: criar um usuário.

Passo a passo do fluxo

  1. O cliente faz a requisição HTTP
    Um frontend, app mobile ou ferramenta como Postman envia um POST /users.
  2. Controller recebe a requisição
    O Controller é o primeiro ponto de contato. Ele extrai os dados do body e chama o Service.
@Post()
create(@Body() data: CreateUserDto) {
  return this.usersService.createUser(data);
}
  1. Service executa a regra de negócio
    O Service decide se a operação pode ou não acontecer. Ele valida regras, aplica lógica e coordena o processo.
async createUser(data: CreateUserDto) {
  const exists = await this.usersRepository.findByEmail(data.email);

  if (exists) {
    throw new Error('Usuário já cadastrado');
  }

  return this.usersRepository.create(data);
}
  1. Repository acessa os dados
    O Repository conversa com o banco de dados, executa queries e retorna o resultado.
async create(data: CreateUserDto) {
  return this.prisma.user.create({ data });
}
  1. Resposta sobe pelas camadas
    O resultado volta para o Service, depois para o Controller e, por fim, para o cliente.

Por que esse fluxo é tão importante?

Esse fluxo garante:

  • Código organizado e previsível
  • Facilidade para debugar problemas
  • Camadas bem isoladas
  • Manutenção e evolução mais seguras

Se amanhã você precisar mudar o banco de dados, a regra de negócio continua intacta. Se mudar o formato da API, o domínio e os repositories continuam funcionando.

Um erro comum: pular camadas

Um erro clássico é o Controller chamar diretamente o Repository. Isso pode parecer mais rápido, mas quebra completamente a arquitetura. Com o tempo, o projeto vira um emaranhado difícil de manter.

A Layered Architecture funciona bem justamente porque respeita esse fluxo.

Vantagens e desvantagens da Layered Architecture

A Layered Architecture é extremamente popular, mas isso não significa que ela seja perfeita para todos os cenários. Como qualquer padrão arquitetural, ela tem pontos fortes e limitações. Entender isso evita tanto o uso errado quanto o excesso de complexidade.

Vantagens da arquitetura em camadas

1. Código mais organizado e legível
Cada camada tem um papel claro. Quando alguém novo entra no projeto, fica muito mais fácil entender onde cada coisa está.

2. Separação de responsabilidades
Controller cuida de entrada e saída, Service cuida da regra de negócio, Repository cuida dos dados. Isso reduz acoplamento e confusão.

3. Facilidade para testar
Você pode testar Services sem precisar subir servidor ou banco de dados, usando mocks de repositories.

4. Manutenção mais segura
Mudanças em uma camada dificilmente quebram outra. Isso é essencial em projetos que crescem com o tempo.

5. Adoção natural em frameworks modernos
Frameworks como NestJS, Spring e ASP.NET Core já incentivam esse padrão, o que facilita boas práticas desde o início.

Desvantagens e cuidados

1. Pode virar burocracia em projetos pequenos
Para APIs muito simples, criar várias camadas pode ser exagero e atrasar entregas.

2. Risco de Services “gordos”
Se não houver cuidado, o Service pode virar um arquivo gigante com regras demais. Nesses casos, vale pensar em dividir responsabilidades.

3. Nem sempre reflete bem domínios complexos
Em sistemas muito grandes, a Layered Architecture pode não ser suficiente sozinha. Outras abordagens, como Clean Architecture ou Hexagonal, podem fazer mais sentido.

4. Falsa sensação de boa arquitetura
Só separar em camadas não garante código de qualidade. Se a regra de negócio estiver espalhada ou mal escrita, o problema continua.

Quando usar Layered Architecture?

Ela é uma ótima escolha quando:

  • Você está aprendendo arquitetura
  • Está construindo APIs REST
  • Trabalha em times pequenos ou médios
  • Usa frameworks como NestJS

Ela é simples, poderosa e resolve a maioria dos problemas do dia a dia.

Boas práticas para usar Layered Architecture no NestJS

Aplicar Layered Architecture no NestJS é relativamente simples, mas fazer isso bem feito exige alguns cuidados. Aqui não é sobre seguir regras rígidas, e sim sobre criar um código que faça sentido, seja fácil de manter e não vire um problema no futuro.

Mantenha Controllers o mais simples possível

Um bom sinal de que o Controller está bem escrito é quando ele parece “sem graça”. Ele não deve conter lógica complexa, loops, regras ou decisões importantes.

Se você olha para um Controller e vê muita lógica, provavelmente essa lógica deveria estar no Service.

Services focados em um único contexto

Evite Services genéricos demais, como UserService fazendo tudo relacionado ao usuário. Conforme o projeto cresce, vale dividir em serviços mais específicos, como:

  • CreateUserService
  • ListUserService
  • UpdateUserService

Isso evita arquivos gigantes e melhora a leitura do código.

Repository como contrato, não como atalho

O Repository não é um atalho para o banco. Ele é uma camada de abstração. Prefira métodos claros como:

  • findByEmail
  • findById
  • save

Evite métodos genéricos demais que recebem queries prontas do Service.

Não exagere na abstração

Um erro comum de quem está aprendendo arquitetura é criar camadas demais sem necessidade. Se o projeto é pequeno, tudo bem simplificar. Arquitetura boa é aquela que resolve problemas reais, não a mais bonita no papel.

Estrutura de pastas sugerida

Uma estrutura simples e eficiente no NestJS pode ser:

  • users
    • users.controller.ts
    • users.service.ts
    • users.repository.ts
    • dto
    • entities

Essa organização já aplica Layered Architecture de forma clara e prática.

Pense em testes desde o início

Quando as camadas estão bem separadas, escrever testes unitários fica muito mais fácil. Services testáveis são um ótimo sinal de que a arquitetura está bem definida.

Conclusão

A Layered Architecture não é um conceito novo, nem algo exclusivo de grandes empresas, mas continua sendo um dos design patterns arquiteturais mais importantes para quem desenvolve aplicações back-end. Ela resolve um problema muito comum, principalmente entre iniciantes: a falta de organização e a mistura de responsabilidades no código.

Ao separar a aplicação em Controllers, Services, Repositories e, quando necessário, Domínio, você ganha clareza. Fica mais fácil entender o que o sistema faz, onde cada regra deve ficar e como evoluir o projeto sem medo de quebrar tudo. No NestJS, essa arquitetura surge quase de forma natural, o que ajuda bastante quem está começando a escrever código mais profissional.

O ponto mais importante é entender que arquitetura não é sobre seguir padrões cegamente. É sobre escolher soluções que façam sentido para o problema que você está resolvendo. Em projetos pequenos, a arquitetura em camadas pode ser simples. Em projetos maiores, ela se torna essencial para manter o código saudável ao longo do tempo.

Se você está aprendendo back-end, dominou Node.js ou está começando com NestJS, entender Layered Architecture é um passo enorme na sua evolução como desenvolvedor. Ela aparece no dia a dia de empresas reais, em code reviews e, com muita frequência, em entrevistas técnicas.

Agora quero saber de você:
Você já usa arquitetura em camadas nos seus projetos ou ainda mistura tudo no mesmo lugar? Já passou por algum problema por falta de organização no código?

Deixa seu comentário aqui embaixo e compartilha sua experiência. Essa troca é o que faz a comunidade dev evoluir junto.