Autorização faz parte do domínio, não do controller
- backend
- domínio
- autorização
- arquitetura
Quando um sistema começa pequeno, a autorização quase sempre nasce como um
if no controller: o usuário está logado? é dono do recurso? tem o papel
certo? À medida que o domínio cresce, esses ifs se multiplicam, se
contradizem e passam a viver longe das regras de negócio que deveriam proteger.
Este post é sobre um princípio simples: quem pode fazer o quê é uma regra de domínio, e regras de domínio pertencem ao modelo — não à borda HTTP.
O problema do if espalhado
Considere um endpoint que arquiva um projeto. A primeira versão costuma ser assim:
async function archiveProject(req: Request, res: Response) {
const project = await projects.findById(req.params.id);
if (!project) return res.status(404).end();
// Regra de autorização perdida no meio do controller.
if (project.ownerId !== req.user.id && req.user.role !== "admin") {
return res.status(403).end();
}
project.archivedAt = new Date();
await projects.save(project);
res.json(project);
}O problema não é o código em si — é onde ele mora. Essa mesma regra ("dono ou admin") precisa ser repetida em cada endpoint que mexe no projeto: editar, excluir, convidar membros. Uma hora alguém esquece um dos lugares, e você tem uma falha de autorização silenciosa.
Movendo a decisão para o domínio
A pergunta "este usuário pode arquivar este projeto?" só depende de dois
objetos do domínio: o usuário e o projeto. Ela não precisa saber nada
sobre HTTP, req ou res. Então ela pertence ao domínio:
class Project {
constructor(
readonly id: string,
readonly ownerId: string,
private archivedAt: Date | null,
) {}
canBeArchivedBy(user: User): boolean {
return this.ownerId === user.id || user.isAdmin();
}
archive(user: User): void {
if (!this.canBeArchivedBy(user)) {
throw new ForbiddenError("Sem permissão para arquivar este projeto.");
}
this.archivedAt = new Date();
}
}Agora o controller volta a fazer só o trabalho dele — traduzir HTTP para domínio e de volta:
async function archiveProject(req: Request, res: Response) {
const project = await projects.findById(req.params.id);
if (!project) return res.status(404).end();
project.archive(req.user); // a regra vive aqui dentro
await projects.save(project);
res.json(project);
}A borda HTTP não decide se algo pode acontecer. Ela só pergunta ao domínio e converte a resposta em um status code.
Por que isso importa
Mover a autorização para o modelo traz três ganhos concretos:
- Um único lugar para a regra. Toda operação sobre o projeto passa por
archive/canBeArchivedBy. Não há como esquecer de checar em um endpoint novo — o método simplesmente não deixa. - Testável sem HTTP. Você testa
canBeArchivedBycom objetos puros, sem subir um servidor nem simularreq/res. - Lê como o negócio fala.
project.archive(user)descreve a intenção;if (project.ownerId !== req.user.id && ...)descreve mecânica.
Onde parar
Isso não quer dizer jogar toda autorização no modelo. Regras transversais — "a conta está suspensa?", "o token expirou?" — são melhores como middleware, antes de chegar no domínio. A distinção que uso:
| Tipo de regra | Onde vive |
|---|---|
| Depende de quem é o usuário | Middleware |
| Depende do estado de uma entidade | Modelo de domínio |
Autorização baseada em estado do domínio ("dono do projeto", "membro da equipe", "autor do comentário") pertence ao domínio. Autorização baseada em identidade ("autenticado", "não banido") pertence à borda.
A regra prática: se responder à pergunta exige carregar uma entidade e olhar seus campos, a decisão mora junto dessa entidade.