A rota de API que virou um monólito
Começou como um handler. Dois anos depois é um arquivo de 900 linhas que ninguém ousa tocar. Veja como evitar isso.
Consigo descrever o arquivo antes de abri-lo. Chama-se route.ts, tem entre 600 e 1.200 linhas e começou como um handler de quinze linhas que salvava um formulário. Em algum momento ganhou um switch sobre o corpo da requisição, três clientes de banco, uma chamada de email, um webhook para o CRM e um comentário dizendo 'temporário'.
Já li esse arquivo em dezenas de bases de código. É a falha arquitetural mais comum em aplicações Next.js, e acontece porque o framework torna fácil colocar código em algum lugar sem decidir onde ele pertence.
Como acontece
O App Router te dá route handlers e Server Actions como lugares para rodar código de servidor. Os dois são pontos de entrada. Nenhum é lugar para regra de negócio, mas nada te impede de colocar lá, e a primeira versão é sempre pequena o bastante para parecer ok.
Aí a segunda feature precisa da mesma validação, então ela é copiada. A terceira precisa de uma versão um pouco diferente, então o handler ganha um parâmetro. A quarta precisa rodar depois da terceira, então elas são encadeadas com uma flag. Cada passo é uma decisão local razoável. A soma é um monólito sem fronteiras, morando dentro de um arquivo cujo único trabalho era parsear uma requisição.
O sinal é quando o route handler é importado por outra coisa. Um ponto de entrada não deveria ter chamadores além do framework. No momento em que outro módulo precisa da lógica que está lá dentro, a lógica está no lugar errado.
A estrutura que aguenta
Minha regra é que um ponto de entrada faz exatamente quatro coisas: autenticar, validar a entrada, chamar uma função e moldar a resposta. Dez a trinta linhas. Se está mais longo, alguma coisa vazou para dentro.
A função que ele chama vive em um módulo de domínio: uma função TypeScript comum que recebe entrada tipada e retorna resultado tipado, e não sabe nada sobre HTTP, formulários ou o framework. Pode ser chamada de um route handler, de uma Server Action, de um worker de fila e de um teste, e se comporta do mesmo jeito nos quatro.
Essa separação é o truque inteiro. O route handler e a Server Action da mesma operação viram wrappers finos ao redor da mesma função de domínio. Quando um app mobile precisa de uma API, você adiciona um terceiro wrapper. Quando um cron job precisa rodar a operação toda noite, um quarto. A lógica nunca se move.
Abaixo do módulo de domínio fica a camada de acesso a dados. Mantenho as queries em módulos próprios, nomeados pelo agregado que tocam, e a função de domínio as chama pelo nome. O domínio nunca monta uma query. É isso que impede o ORM de se espalhar por todos os arquivos.
Para onde vão os efeitos colaterais
A rota monolítica costuma conter efeitos colaterais inline: enviar o email, chamar o CRM, atualizar o índice de busca. Cada um adiciona latência à requisição e um jeito novo de falhar no meio do caminho.
Efeitos colaterais pertencem a um outbox ou a uma fila. A função de domínio escreve o registro e uma linha de evento na mesma transação. Um worker lê os eventos e executa os efeitos, com retries. A requisição retorna assim que o registro é commitado. O usuário não espera o CRM, e o CRM fora do ar não derruba o checkout.
Em um projeto pequeno, o worker pode ser um route handler disparado por um agendador. Em um maior, é um consumidor de fila de verdade. A função de domínio não muda em nenhum dos casos.
Refatorando o que você já tem
Não reescreva. Extraia de dentro para fora. Ache o menor bloco autocontido, geralmente a validação, e mova para um módulo com teste. Depois o próximo bloco. A rota fica mais curta a cada pull request, e continua funcionando o tempo inteiro.
Configuro uma regra de lint que falha quando um arquivo dentro do diretório app passa de um número de linhas, e deixo rígida o bastante para doer. Limites que doem são respeitados. Limites confortáveis são ignorados, e o arquivo cresce de novo.
O monólito não é um problema do Next.js. É um problema de posicionamento. Decida onde a lógica vive antes de escrevê-la, e o ponto de entrada continua sendo o que deveria ser: uma porta, não uma casa.