"Configuração do projeto" soa como uma lista de verificação de arquivos a serem criados, mas as escolhas por trás dela - um serviço por repositório ou muitos, saída compilada verificada ou não, um layout feito à mão ou um gerado por um gerador - todas remontam a um pequeno número de compensações subjacentes.
Esta página é sobre essas compensações: topologia de repositório, a divisão build/runtime e o problema de coordenação que as ferramentas de monorepo existem para resolver.
A estrutura de um projeto codifica três decisões separáveis - o que conta como um implantável, onde fica o limite da etapa de compilação e se um histórico git contém um serviço ou muitos - e a maioria das perguntas "de qual ferramenta precisamos" são realmente sobre uma dessas três.
Por Que Importa: Tomar a decisão errada de topologia em qualquer direção custa tempo real - um monorepo prematuro sobrecarrega cada PR com sobrecarga de coordenação, enquanto uma divisão de serviço tardia significa desembaraçar código compartilhado que já está acoplado em produção.
Conceitos-Chave:topologia de repositório, limite implantável, divisão build/runtime, grafo de tarefas, scaffolding, CI baseada em affected.
Quando Usar Este Modelo: Decidir se um novo serviço precisa de seu próprio repositório, escolher quando introduzir workspaces ou um orquestrador de tarefas, raciocinar sobre por que src/ e dist/ são separados e entender o que um gerador ou repositório de modelo está realmente padronizando.
Limitações / Compensações: Nenhuma estrutura é gratuita - um monorepo troca a simplicidade por serviço por custo de coordenação transversal, e um repositório de serviço único troca esse custo de coordenação por duplicação de código eventual quando um segundo implantável aparece.
Tópicos Relacionados: npm workspaces, Turborepo, Nx, limites de serviço, scaffolding e templates.
Por baixo dos nomes das pastas, a estrutura de um projeto está realmente codificando três coisas.
A primeira é um limite implantável: o que conta como uma unidade de código independentemente executável e implantável - um package.json, um build, um processo que é implantado como um todo.
A segunda é a divisão build/runtime: a linha entre o código fonte que você edita (src/, TypeScript) e o artefato que realmente é executado em produção (dist/, JavaScript compilado) - uma divisão que existe porque o Node não executa TypeScript nativamente na maioria das configurações de produção, então a compilação é uma etapa de tempo de build, nunca de tempo de execução.
A terceira é a topologia de repositório: se um implantável vive sozinho em seu próprio histórico git (um repositório de serviço único) ou múltiplos implantáveis e pacotes compartilhados vivem juntos em um histórico git, coordenados através de workspaces (um monorepo).
Essas três decisões são independentes umas das outras em princípio - você pode ter um monorepo sem ferramentas de build compartilhadas, ou um repositório de serviço único com um pipeline de build elaborado - mas na prática elas tendem a andar juntas, porque as ferramentas que suportam uma frequentemente assumem as outras.
Uma analogia útil: a estrutura de um repositório é a planta de um edifício.
A planta não faz nada por si só, mas decide onde o novo trabalho deve ir - uma nova tomada elétrica tem um lugar óbvio para se conectar porque o plano de fiação já existe - que é exatamente o que impede uma equipe de inventar sua própria fiação à medida que avança.
A topologia do repositório leva diretamente à escolha da ferramenta, e entender essa cascata explica por que certas ferramentas aparecem juntas.
Um repositório de serviço único não precisa de nada além de um gerenciador de pacotes - há apenas um package.json, um grafo de dependências, um build.
Um monorepo precisa de workspaces (protocolo de workspace do npm, pnpm ou Yarn - veja Workspaces & Monorepos) no mínimo, porque os workspaces são o que permitem que um pacote dependa de outro pacote no mesmo repositório sem publicá-lo em um registro primeiro.
Mas os workspaces sozinhos resolvem apenas o link - eles dizem ao gerenciador de pacotes onde os pacotes locais residem, não quais realmente mudaram, e não como evitar reconstruir ou retestar pacotes que não mudaram.
Essa é a lacuna específica que orquestradores de tarefas como Turborepo e Nx preenchem: ambos constroem um grafo de dependência entre pacotes de workspace (não apenas um grafo de link, um grafo de tarefas - "construir api requer que shared já esteja construído") e o usam para executar apenas o que uma determinada alteração realmente afeta, armazenando em cache o resultado de todo o resto.
É por isso que um monorepo sem um orquestrador de tarefas ainda funciona corretamente, apenas mais devagar à medida que cresce - o orquestrador é uma camada de desempenho e coordenação sobre os workspaces, não um substituto para eles.
Scaffolding é o mecanismo que transforma uma estrutura escolhida em algo que é replicado consistentemente, em vez de ser redecidido por quem cria o próximo serviço.
npm init, uma CLI de framework (como o gerador NestJS), ou um repositório de modelo interno estão realizando o mesmo trabalho conceitual em diferentes níveis de opinião: codificando uma decisão de estrutura uma vez para que não precise ser discutida, ou sutilmente desviada, toda vez que alguém inicia um novo projeto.
// turbo.json - codifica o grafo de tarefas, não apenas os links de workspace{ "tasks": { "build": { "dependsOn": ["^build"], "outputs": ["dist/**"] }, "test": { "dependsOn": ["build"] } }}
A descoberta de configuração também faz parte das mecânicas, não apenas conveniência humana: ferramentas como tsc, ESLint e Docker procuram seus arquivos de configuração subindo na árvore de diretórios de onde são invocadas, que é exatamente por que tsconfig.json, package.json e Dockerfile convencionalmente residem na raiz de um repositório (ou pacote de workspace) - a colocação é como eles são encontrados, não apenas onde são organizados para legibilidade.
As ferramentas de monorepo justificam seu custo em uma curva, não em um limiar - o momento certo para adotar Turborepo ou Nx se correlaciona com a contagem de implantáveis, o tempo de build e a frequência com que um pacote compartilhado muda em sincronia com seus consumidores, não com um número fixo de pacotes.
Monorepo Multi-Serviço cobre as regras de limite específicas (o que pertence a apps/ vs packages/, quando um pacote deve ser extraído) que tornam esse julgamento concreto.
O caminho de adoção realista é incremental, não uma única decisão em "big bang": as equipes geralmente começam com um repositório de serviço único, adicionam um segundo implantável e buscam workspaces simples quando o código precisa ser compartilhado, e só adicionam um orquestrador de tarefas quando o tempo de build ou CI - não apenas a contagem de pacotes - se torna o problema real.
Saltar direto para um monorepo com muitas ferramentas para um repositório de dois pacotes geralmente adiciona sobrecarga de configuração e cognitiva sem um benefício correspondente ainda.
CI é onde essa decisão se agrava mais visivelmente: um pipeline de repositório completo reconstrói e retesta tudo em cada alteração, enquanto CI baseada em affected - que tanto Turborepo quanto Nx suportam - usa o mesmo grafo de tarefas para executar apenas o que os arquivos alterados por um determinado commit poderiam ter tocado, que é a diferença entre um pipeline de cinco minutos e um de quarenta minutos assim que um monorepo tem uma dúzia de pacotes.
Há uma dimensão de governança que vale a pena nomear também: estrutura consistente entre serviços (o mesmo lugar para src/server.ts, os mesmos nomes de script, a mesma convenção de health-check) é o que torna o plantão e o onboarding rápidos em uma organização com muitos serviços - qualquer engenheiro que trabalhou em um repositório de serviço pode navegar em outro, que é o retorno real do scaffolding além de economizar digitação no primeiro dia.
Camada
Força
Fraqueza
Melhor Ajuste
Repositório de serviço único, sem orquestrador
Configuração mais simples possível, zero sobrecarga de coordenação
Duplicação de código assim que um segundo implantável aparece
Um implantável, equipe pequena
Apenas Workspaces (sem orquestrador de tarefas)
Compartilha código entre pacotes com um único lockfile
Sem cache ou CI baseada em affected - reconstruções completas a cada vez
Monorepo pequeno, 2-4 pacotes, tempos de build toleráveis
Turborepo
Configuração de pipeline simples, rápida de adotar incrementalmente
Menos opinativo sobre organização de código do que Nx
Equipes que desejam cache/builds affected sem um framework
Nx
Geradores, limites de módulo impostos, integração profunda de ferramentas
Curva de aprendizado mais acentuada, mais estrutura inicial
Organizações maiores que desejam convenções impostas entre muitas equipes
"Um monorepo significa apenas múltiplos repositórios gerenciados juntos em git." É o oposto - um histórico git contendo múltiplos pacotes independentemente implantáveis ou publicáveis, coordenados através de workspaces e, geralmente, um orquestrador de tarefas.
"Você precisa de Turborepo ou Nx no momento em que tem dois pacotes." Workspaces simples são suficientes até que o tempo de build ou teste, ou a falta de detecção de alterações, se torne um gargalo real - adotar um orquestrador mais cedo apenas adiciona configuração para manter.
"src/ vs dist/ é uma preferência de estilo." Marca o limite da etapa de compilação - o que é editado à mão versus o que realmente é enviado e executado em produção - e confundir os dois facilita a implantação acidental de código obsoleto ou não compilado.
"Ferramentas de Scaffolding são apenas modelos iniciais opcionais." Elas são como uma organização impõe consistência estrutural no momento em que um projeto é criado, o que é muito mais barato do que corrigir a deriva estrutural em uma dúzia de serviços mais tarde.
"Um monorepo elimina a necessidade de pensar sobre versionamento entre pacotes." Pacotes internos podem permanecer em workspace:* indefinidamente dentro do repositório, mas no momento em que qualquer pacote é publicado ou consumido fora do monorepo, a disciplina semver real volta à mesa.
Quais três decisões a "estrutura do projeto" realmente codifica?
Limites implantáveis (o que é uma unidade independentemente implantada), a divisão build/runtime (código fonte vs saída compilada) e a topologia de repositório (repositório de serviço único vs monorepo) - a maioria das perguntas concretas de estrutura se resume a uma dessas três.
Por que o código Node precisa de uma divisão `src/` e `dist/`?
Porque o TypeScript precisa de uma etapa de compilação antes de se tornar JavaScript simples que o Node pode executar na maioria das configurações de produção - src/ é o que é editado, dist/ é o artefato que é realmente implantado e executado.
Os npm workspaces sozinhos me dão um sistema de build de monorepo?
Eles fornecem link de pacotes - um pacote pode depender de outro no mesmo repositório sem publicá-lo - mas eles não computam o que mudou ou armazenam em cache os resultados do build, que é o trabalho específico que um orquestrador de tarefas como Turborepo ou Nx faz por cima.
Quando vale a pena adotar Turborepo ou Nx?
Quando os builds de workspace simples começam a levar tempo real porque tudo é reconstruído a cada alteração, ou quando uma contagem crescente de pacotes torna difícil raciocinar manualmente sobre "o que realmente precisa ser executado" - não simplesmente em uma contagem fixa de pacotes.
Por que arquivos de configuração como `tsconfig.json` convencionalmente residem na raiz do repositório?
Porque as ferramentas os procuram subindo na árvore de diretórios de onde são invocadas - a colocação na raiz não é apenas convenção para humanos, é como a ferramenta realmente encontra o arquivo.
Qual é a diferença prática entre Turborepo e Nx?
Ambos constroem um grafo de tarefas e armazenam resultados em cache, mas o Nx adiciona geradores e regras de limite de módulo impostas por cima, o que se adequa a organizações maiores que desejam consistência entre muitas equipes, enquanto o Turborepo permanece mais leve e rápido de adotar incrementalmente.
Começar com um repositório de serviço único é a escolha "errada" se o projeto puder crescer?
Não - geralmente é o padrão correto, pois um monorepo prematuro sobrecarrega cada PR inicial com sobrecarga de coordenação que não compensa até que haja um segundo implantável realmente compartilhando código.
O que significa "CI baseada em affected" na prática?
O pipeline de CI usa o mesmo grafo de tarefas que o orquestrador usa para builds locais para descobrir quais pacotes um determinado commit poderia ter tocado, e apenas constrói/testa esses - em vez de reconstruir e retestar todo o repositório em cada alteração.
Qual é o valor real de uma ferramenta de scaffolding ou repositório de modelo além de economizar digitação?
Ele codifica decisões estruturais - layout de pastas, nomes de scripts, configuração de lint, configuração do Docker - uma vez, para que cada novo serviço comece consistente em vez de divergir ligeiramente do que a última pessoa que configurou um lembrou de incluir.
Um monorepo significa que pacotes internos nunca precisam de números de versão?
Apenas enquanto permanecerem puramente internos - workspace:* resolve para a cópia local sem nunca tocar no semver, mas no momento em que um pacote é publicado externamente ou consumido fora do monorepo, a disciplina de versão real se aplica novamente.
Como sei quando extrair um pacote compartilhado em vez de copiar código?
Uma regra geral comum é tolerar a duplicação até que um terceiro consumidor precise do mesmo código - extrair após a segunda cópia é frequentemente prematuro, e esperar pela duplicação real e repetida evita adivinhar uma abstração muito cedo.