Blog /desenvolvimento

Padrão MVC na prática: Model, View e Controller antes do framework

MVC não é coisa de framework. É o padrão que aparece em todo backend. Entender de verdade muda onde você coloca cada peça do sistema.

7 min
Antes do Framework — série sobre fundamentos de desenvolvimento

Antes do Framework — Ep. 09

Você já sabe como o código pensa em diferentes paradigmas. Agora o problema é outro: onde colocar cada coisa quando o sistema cresce.

Todo dev que colocou lógica de negócio direto no controller, query SQL dentro da view, ou validação espalhada em três lugares diferentes já sentiu a dor que o MVC existe para evitar.


O problema que o MVC resolve

Imagina um sistema de cadastro de pedidos sem nenhuma organização:

# tudo em um arquivo só
@app.route('/pedidos', methods=['POST'])
def criar_pedido():
    dados = request.json

    # validação misturada com rota
    if not dados.get('cliente_id'):
        return {'erro': 'cliente obrigatório'}, 400

    # query direto no handler
    cliente = db.execute('SELECT * FROM clientes WHERE id = ?', dados['cliente_id']).fetchone()
    if not cliente:
        return {'erro': 'cliente não encontrado'}, 404

    # regra de negócio no meio da rota
    if dados['total'] > 10000 and cliente['limite'] < dados['total']:
        return {'erro': 'limite excedido'}, 422

    db.execute('INSERT INTO pedidos (cliente_id, total) VALUES (?, ?)', ...)
    return {'mensagem': 'pedido criado'}, 201

Funciona. Mas quando você precisar reutilizar essa lógica em outro endpoint, testar o comportamento do limite de crédito, ou mudar o banco de dados, tudo isso vai exigir mexer nesse arquivo. E encontrar o bug vai ser uma aventura.


O que é MVC

Model, View, Controller é uma forma de separar responsabilidades.

Cada camada tem uma função clara:

CamadaResponsabilidade
ModelDados e regras de negócio. O "o quê" do sistema.
ViewApresentação. O que o usuário vê (HTML, JSON, XML).
ControllerOrquestração. Recebe a requisição, chama o Model, devolve a View.

A ideia: o Controller não sabe como os dados são persistidos. O Model não sabe como a resposta vai ser formatada. A View não sabe de onde vieram os dados.


O mesmo sistema, organizado

# models/pedido.py — regras de negócio
class PedidoService:
    def __init__(self, db):
        self.db = db

    def criar(self, cliente_id, total):
        cliente = self.db.buscar_cliente(cliente_id)
        if not cliente:
            raise ClienteNaoEncontrado()

        if total > 10000 and cliente.limite < total:
            raise LimiteExcedido()

        return self.db.inserir_pedido(cliente_id, total)
# controllers/pedido_controller.py — só orquestra
@app.route('/pedidos', methods=['POST'])
def criar_pedido():
    dados = request.json

    if not dados.get('cliente_id'):
        return {'erro': 'cliente obrigatório'}, 400

    try:
        pedido = pedido_service.criar(dados['cliente_id'], dados['total'])
        return {'id': pedido.id, 'mensagem': 'pedido criado'}, 201
    except ClienteNaoEncontrado:
        return {'erro': 'cliente não encontrado'}, 404
    except LimiteExcedido:
        return {'erro': 'limite excedido'}, 422

Agora a regra do limite de crédito está no PedidoService. Você consegue testá-la sem subir um servidor, reutilizá-la em outro endpoint e entender onde ela vive sem precisar ler código de rota.


Como os frameworks implementam isso

Django (Python)

  • Model: classes models.py com ORM embutido
  • View: funções ou classes em views.py (o que o Django chama de view é o que outros chamam de controller)
  • Template: os arquivos HTML — o Django separa controller (view) de apresentação (template)

Spring Boot (Java)

  • @Entity: o Model
  • @Service: onde fica a lógica de negócio
  • @Controller / @RestController: recebe requisição e devolve resposta
  • @Repository: acesso ao banco separado do serviço

Laravel (PHP)

  • Model: Eloquent ORM
  • Controller: processa requisição
  • View: Blade templates
  • Service: camada adicional que muitos adicionam para separar lógica do controller

Cada framework tem suas nomenclaturas, mas a ideia central é a mesma. Quando você entende o padrão, qualquer framework novo fica mais fácil de navegar.


Além do MVC: camadas que aparecem em sistemas maiores

O MVC resolve a separação básica, mas sistemas maiores adicionam camadas:

Repository: isola o acesso ao banco. O Service não faz query direto, chama o Repository.

class PedidoRepository:
    def buscar_por_cliente(self, cliente_id):
        return db.execute('SELECT * FROM pedidos WHERE cliente_id = ?', cliente_id)

Service / Use Case: onde a lógica de negócio real vive. O Controller chama o Service, não o banco.

DTO (Data Transfer Object): objetos que carregam dados entre camadas sem expor o Model diretamente. Útil para validação e transformação antes de chegar na lógica.

Você não precisa implementar tudo isso no primeiro projeto. Mas saber que existe evita que você coloque query SQL dentro do controller "só por enquanto" e ache lá dois anos depois.


O erro mais comum com MVC

Controller gordo. É quando toda a lógica vai para o controller porque é o lugar mais óbvio para colocar.

A regra prática: se o controller tem mais de 20 linhas de lógica real (sem contar validação de entrada e tratamento de erro), alguma coisa deveria estar num Service.

Controller deve ser fino: recebe, valida o formato, delega, responde.


→ Próximo episódio

Você sabe como organizar o código. Agora vem o lugar onde a maioria dos devs tem o maior ponto cego: o banco de dados. Não o ORM — o SQL real. Porque quando o ORM gera uma query que trava a produção, é você quem vai precisar lê-la e corrigir.

Antes do Framework — Ep. 10: SQL antes do ORM — o que o ActiveRecord, Eloquent e Hibernate escondem de você

Gostou do artigo?

Newsletter

Em breve

Em breve você poderá receber novos artigos direto no seu email.