# Como estruturar um monorepo .NET + Next.js sem virar bagunça

> Duas toolchains num repositório funcionam quando cada uma é dona da própria subárvore e só um contrato HTTP atravessa a fronteira. A estrutura, as quatro regras e os três padrões de falha mais caros.

# Como estruturar um monorepo .NET + Next.js sem virar bagunça

Um monorepo poliglota funciona quando cada toolchain é dona da própria subárvore e nada atravessa a fronteira além de um contrato HTTP e um punhado de arquivos de configuração. Ele vira bagunça quando alguém tenta fazer .NET e Node compartilharem um sistema de build.

## A estrutura

```
apps/
  marketing-site/     Next.js: site público, SEO, preços
  web-app/            React: área do cliente autenticada
  admin-panel/        React: operação interna
backend/
  api/
    src/              Solução .NET: Api, Application, Domain, Infrastructure
    tests/            xUnit
config/
  branding.json  product.json  pricing.json  features.json
docs/
scripts/
package.json          npm workspaces, só na raiz
```

Duas toolchains, duas raízes. O `npm` nunca olha dentro de `backend/`; o `dotnet` nunca olha dentro de `apps/`. O diretório `config/` é a única coisa que os dois lados leem.

## Regra 1: um lockfile, na raiz

O npm workspaces coloca um único `package-lock.json` na raiz do repositório e nenhum nas apps individuais. Isso não é estética. Lockfiles por app produzem três resoluções diferentes da mesma dependência transitiva, três conjuntos de alertas de segurança para um único aviso, e uma atualização de dependência que precisa ser aplicada três vezes.

Coloque os lockfiles por app no `.gitignore` para que um `npm install` perdido dentro de `apps/web-app` não consiga commitar um por acidente. Se a sua plataforma de deploy builda uma app isolada, faça com que ela instale a partir do lockfile da raiz em vez de gerar o próprio.

Fixar versões também é assunto da raiz, via `overrides` no `package.json` raiz, para que uma versão forçada valha em todo lugar de uma vez.

## Regra 2: a fronteira entre as stacks é HTTP, e nada mais

A tentação é um pacote de tipos compartilhados, gerado a partir dos records em C# e importado pelo TypeScript. Resista até sentir a dor que justifique isso, porque ele acopla os ciclos de deploy de duas coisas que são publicadas de forma independente.

O que funciona na prática: a API publica um documento OpenAPI, e cada frontend mantém um tipo de resposta pequeno, escrito à mão, perto do código que chama o endpoint. Duplica-se um punhado de declarações de interface. Em troca, os frontends não têm dependência de build com o backend, e uma refatoração no backend não consegue quebrar o build de um frontend.

O corolário é que nenhum frontend importa outro frontend. Três apps que compartilham a mesma linguagem visual vão querer compartilhar componentes, e no momento em que `web-app` importa de `admin-panel`, você não tem mais três apps. Tem uma app com três pontos de entrada e um raio de impacto compartilhado. Ou você duplica o componente, ou promove ele a um pacote de workspace de verdade, com o próprio `package.json`.

## Regra 3: configuração é dado, num lugar só

Nome do produto, cores da marca, nomes dos planos e chaves de feature aparecem no site de marketing, na app do cliente, no painel administrativo, na API e na seed do banco. Cinco cópias são cinco lugares para esquecer.

Coloque tudo em `config/*.json` na raiz e faça todos os consumidores lerem de lá: os frontends importam o JSON direto, a API lê no startup, o script de seed gera a partir dele. Renomear o produto vira um diff de uma linha em vez de uma busca pelo repositório inteiro.

O teste de que isso está funcionando: rebrandear o produto inteiro deve tocar exatamente um arquivo.

## Regra 4: a CI roda por projeto, não tudo ou nada

Um monorepo onde todo push roda a matriz completa, ou seja, três builds de frontend, `dotnet test` e a suíte end-to-end inteira, treina o time a ignorar a CI, porque um erro de digitação num título de marketing leva doze minutos para ser mergeado.

Separe por caminho:

- Mudanças em `apps/*` → lint, checagem de tipos e testes unitários daquela app
- Mudanças em `backend/` → `dotnet build` e `dotnet test`
- Mudanças em `config/` ou em qualquer coisa compartilhada → tudo
- Testes end-to-end → na branch principal e sob demanda, não em cada pull request

A suíte end-to-end é a que precisa mesmo ser controlada. Ela exige banco, API rodando e frontends buildados; é a coisa mais lenta e mais instável que você tem. Rode onde uma falha mereça a atenção de uma pessoa, não em todo push de rascunho.

## O que dá errado de verdade

Três padrões de falha respondem pela maior parte da dor de um monorepo poliglota.

O primeiro é um Dockerfile compartilhado. A API .NET precisa de uma imagem com SDK e de um publish multi-stage. Os frontends precisam de Node, e na maioria das plataformas não precisam de container nenhum. Um Dockerfile servindo aos dois faz cada mudança de backend reconstruir as camadas do frontend. Dê à API o próprio Dockerfile e deixe os frontends publicarem como saída estática ou serverless.

O segundo é o ambiente local que exige tudo. Se mexer no site de marketing significa subir Postgres, a API e outros dois frontends, as pessoas vão evitar o monorepo. Cada app deve rodar sozinha contra uma URL de API configurada, com um único `docker compose up` levantando o banco quando o backend for realmente necessário.

O terceiro é a CI por caminho que dispara de menos em silêncio. As regras acima só são seguras se o gatilho de "compartilhado" estiver de fato completo. Quando `config/` muda e a CI roda só uma app, você publica um rename em dois de três frontends. Mantenha a lista de caminhos compartilhados curta o bastante para ser auditada e, na dúvida, rode tudo.

## Por que monorepo, afinal

O ganho é a mudança atômica. Adicionar um campo na API, expor no painel administrativo e renderizar no site de marketing é um commit, um review, um deploy, um revert. Espalhado em quatro repositórios, são quatro pull requests numa ordem obrigatória e uma tarde ruim se precisar desfazer.

Você paga isso com disciplina de build. As quatro regras acima são o pagamento. Pule elas e você fica com o acoplamento de um monolito somado à complexidade de ferramental dos microsserviços.

