Camada Controller: Recebendo Requisições e Orquestrando Respostas na Prática Já leu

O Papel do Controller na Arquitetura MVC O controller é o intermediário fundamental entre a requisição do cliente e a lógica de negócio da aplicação. Ele recebe dados do usuário, valida entradas, coordena processos na camada de serviço e retorna respostas apropriadas. Em uma arquitetura bem estruturada, o controller não contém lógica complexa — apenas orquestra chamadas para serviços especializados. Sua responsabilidade primária é garantir que cada requisição HTTP seja processada corretamente e que a resposta seja formatada adequadamente para o cliente. Entender o fluxo completo de uma requisição é essencial: cliente → rota → controller → serviço → repository → banco de dados → serviço → controller → resposta. O controller é o maestro que coordena este fluxo sem executar tarefas que não lhe cabem. Recebendo e Validando Requisições Captura de Dados Os dados chegam ao controller através de diferentes fontes: URL (parâmetros), corpo da requisição (payload JSON) e headers. Cada linguagem e framework possui mecanismos específicos para extrair

O Papel do Controller na Arquitetura MVC

O controller é o intermediário fundamental entre a requisição do cliente e a lógica de negócio da aplicação. Ele recebe dados do usuário, valida entradas, coordena processos na camada de serviço e retorna respostas apropriadas. Em uma arquitetura bem estruturada, o controller não contém lógica complexa — apenas orquestra chamadas para serviços especializados. Sua responsabilidade primária é garantir que cada requisição HTTP seja processada corretamente e que a resposta seja formatada adequadamente para o cliente.

Entender o fluxo completo de uma requisição é essencial: cliente → rota → controller → serviço → repository → banco de dados → serviço → controller → resposta. O controller é o maestro que coordena este fluxo sem executar tarefas que não lhe cabem.

Recebendo e Validando Requisições

Captura de Dados

Os dados chegam ao controller através de diferentes fontes: URL (parâmetros), corpo da requisição (payload JSON) e headers. Cada linguagem e framework possui mecanismos específicos para extrair essas informações.

// Express.js com TypeScript
import express, { Request, Response } from 'express';

const app = express();
app.use(express.json());

interface CriarUsuarioDTO {
  nome: string;
  email: string;
  idade: number;
}

app.post('/usuarios', (req: Request, res: Response) => {
  // Captura do corpo da requisição
  const { nome, email, idade }: CriarUsuarioDTO = req.body;

  // Captura de parâmetros de URL
  const id = req.params.id;

  // Captura de query parameters
  const filtro = req.query.filtro;

  // Captura de headers
  const token = req.headers.authorization;

  res.json({ recebido: { nome, email, idade } });
});

app.listen(3000);

Validação de Entrada

Validar dados no controller antes de enviar à camada de serviço é fundamental. A validação deve ser clara e retornar mensagens úteis ao cliente. Frameworks modernos oferecem bibliotecas dedicadas para esta tarefa.

// Usando classe-validator para validação
import { IsEmail, IsString, IsNumber, MinLength } from 'class-validator';
import { validate } from 'class-validator';

class CriarUsuarioDTO {
  @IsString()
  @MinLength(3)
  nome!: string;

  @IsEmail()
  email!: string;

  @IsNumber()
  idade!: number;
}

app.post('/usuarios', async (req: Request, res: Response) => {
  const dto = Object.assign(new CriarUsuarioDTO(), req.body);
  const erros = await validate(dto);

  if (erros.length > 0) {
    return res.status(400).json({
      erro: 'Validação falhou',
      detalhes: erros.map(e => ({
        campo: e.property,
        mensagens: Object.values(e.constraints || {})
      }))
    });
  }

  // Prosseguir com a lógica
  res.status(201).json({ id: 1, ...dto });
});

Orquestrando a Lógica de Negócio

Coordenação entre Camadas

O controller chama serviços, que por sua vez acessam repositories. O controller nunca deve executar queries diretas ou operações de banco de dados. Sua função é coordenar e garantir que erros sejam tratados apropriadamente.

// Serviço de usuário (camada de negócio)
class UsuarioService {
  constructor(private usuarioRepository: UsuarioRepository) {}

  async criarUsuario(dados: CriarUsuarioDTO) {
    const usuarioExistente = await this.usuarioRepository
      .findByEmail(dados.email);

    if (usuarioExistente) {
      throw new Error('Email já cadastrado');
    }

    const usuarioCriado = await this.usuarioRepository
      .save(dados);

    return usuarioCriado;
  }

  async obterUsuarioPorId(id: number) {
    return this.usuarioRepository.findById(id);
  }
}

// Controller orquestrando o serviço
class UsuarioController {
  constructor(private usuarioService: UsuarioService) {}

  async criar(req: Request, res: Response) {
    try {
      const dados = req.body;
      const usuario = await this.usuarioService.criarUsuario(dados);

      res.status(201).json({
        mensagem: 'Usuário criado com sucesso',
        dados: usuario
      });
    } catch (erro) {
      res.status(400).json({
        erro: erro instanceof Error ? erro.message : 'Erro desconhecido'
      });
    }
  }

  async obter(req: Request, res: Response) {
    try {
      const id = parseInt(req.params.id);
      const usuario = await this.usuarioService.obterUsuarioPorId(id);

      if (!usuario) {
        return res.status(404).json({ erro: 'Usuário não encontrado' });
      }

      res.json(usuario);
    } catch (erro) {
      res.status(500).json({ erro: 'Erro ao obter usuário' });
    }
  }
}

Tratamento de Erros e Exceções

Erros devem ser capturados e convertidos em respostas HTTP apropriadas. Código de status, mensagens claras e estrutura consistente melhoram a experiência da API.

// Middleware de tratamento global de erros
class ErrorHandler {
  static handle(erro: Error, res: Response) {
    if (erro.message.includes('Email já cadastrado')) {
      return res.status(409).json({
        codigo: 'CONFLITO',
        mensagem: erro.message
      });
    }

    if (erro.message.includes('não encontrado')) {
      return res.status(404).json({
        codigo: 'NAO_ENCONTRADO',
        mensagem: erro.message
      });
    }

    // Erro genérico
    res.status(500).json({
      codigo: 'ERRO_INTERNO',
      mensagem: 'Ocorreu um erro ao processar a requisição'
    });
  }
}

// Uso no controller
app.post('/usuarios', async (req: Request, res: Response) => {
  try {
    const usuario = await usuarioService.criarUsuario(req.body);
    res.status(201).json(usuario);
  } catch (erro) {
    ErrorHandler.handle(erro as Error, res);
  }
});

Formatando e Retornando Respostas

O controller é responsável pela estrutura final da resposta. Códigos HTTP corretos, headers apropriados e formato consistente são essenciais para uma API profissional. A resposta deve sempre considerar o contexto da operação: sucesso, validação ou erro.

// Resposta estruturada com padrão consistente
interface RespostaAPI<T> {
  sucesso: boolean;
  dados?: T;
  erro?: string;
  timestamp: string;
}

class UsuarioController {
  private formataResposta<T>(
    dados: T | null,
    erro?: string
  ): RespostaAPI<T> {
    return {
      sucesso: !erro,
      dados: dados ?? undefined,
      erro,
      timestamp: new Date().toISOString()
    };
  }

  async listar(req: Request, res: Response) {
    try {
      const usuarios = await this.usuarioService.listarTodos();
      const resposta = this.formataResposta(usuarios);
      res.json(resposta);
    } catch (erro) {
      const resposta = this.formataResposta(
        null,
        'Erro ao listar usuários'
      );
      res.status(500).json(resposta);
    }
  }

  async atualizar(req: Request, res: Response) {
    try {
      const id = parseInt(req.params.id);
      const usuarioAtualizado = await this.usuarioService
        .atualizar(id, req.body);

      const resposta = this.formataResposta(usuarioAtualizado);
      res.json(resposta);
    } catch (erro) {
      const resposta = this.formataResposta(null, 'Falha na atualização');
      res.status(400).json(resposta);
    }
  }
}

Conclusão

Os pontos principais para dominar a camada controller são: (1) receba dados de múltiplas fontes e valide-os rigorosamente antes de processar — erros detectados cedo economizam tempo e recursos; (2) orquestre a comunicação entre requisição e serviços, nunca executando lógica complexa diretamente no controller — ele deve ser um maestro, não um músico; (3) formate respostas consistentemente com códigos HTTP apropriados e estruturas padronizadas — a qualidade da resposta reflete a qualidade da sua API.

Um controller bem escrito é enxuto, testável e fácil de manter. Investir tempo estruturando-o adequadamente economiza horas de debugging posterior.

Referências


Artigos relacionados