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.

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:
| Camada | Responsabilidade |
|---|---|
| Model | Dados e regras de negócio. O "o quê" do sistema. |
| View | Apresentação. O que o usuário vê (HTML, JSON, XML). |
| Controller | Orquestraçã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.pycom 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 ORMController: processa requisiçãoView: Blade templatesService: 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.
Newsletter
Em breveEm breve você poderá receber novos artigos direto no seu email.