TypeScript

O que Todo Dev Deve Saber sobre Turborepo com TypeScript: Monorepo de Alta Performance

13 min de leitura

O que Todo Dev Deve Saber sobre Turborepo com TypeScript: Monorepo de Alta Performance

Entendendo Monorepos e o Papel do Turborepo Um monorepo é um repositório único que contém múltiplos projetos independentes, seja aplicações web, bibliotecas internas, ferramentas CLI ou pacotes npm. Diferente da abordagem tradicional de polyrepo (um repositório por projeto), o monorepo centraliza o controle de versão, facilitando o compartilhamento de código, manutenção de dependências e sincronização entre projetos relacionados. O Turborepo é um orquestrador de compilação e tarefas construído especificamente para otimizar o desempenho em monorepos. Ele resolve dois problemas críticos: evita reprocessamento desnecessário através de cache inteligente e executa tarefas em paralelo respeitando dependências entre pacotes. Quando você trabalha com dezenas de pacotes, essas otimizações fazem diferença mensurável — builds que levavam minutos caem para segundos. Configuração Inicial: Estrutura e Setup Criando a Estrutura Base Comece criando um novo workspace monorepo com TypeScript. A estrutura padrão segue uma organização clara onde cada pacote é autossuficiente mas compartilha configurações globais: Agora crie a estrutura de diretórios: A raiz do monorepo contém

<h2>Entendendo Monorepos e o Papel do Turborepo</h2>

<p>Um monorepo é um repositório único que contém múltiplos projetos independentes, seja aplicações web, bibliotecas internas, ferramentas CLI ou pacotes npm. Diferente da abordagem tradicional de polyrepo (um repositório por projeto), o monorepo centraliza o controle de versão, facilitando o compartilhamento de código, manutenção de dependências e sincronização entre projetos relacionados.</p>

<p>O Turborepo é um orquestrador de compilação e tarefas construído especificamente para otimizar o desempenho em monorepos. Ele resolve dois problemas críticos: evita reprocessamento desnecessário através de cache inteligente e executa tarefas em paralelo respeitando dependências entre pacotes. Quando você trabalha com dezenas de pacotes, essas otimizações fazem diferença mensurável — builds que levavam minutos caem para segundos.</p>

<h2>Configuração Inicial: Estrutura e Setup</h2>

<h3>Criando a Estrutura Base</h3>

<p>Comece criando um novo workspace monorepo com TypeScript. A estrutura padrão segue uma organização clara onde cada pacote é autossuficiente mas compartilha configurações globais:</p>

<pre><code class="language-bash">mkdir meu-monorepo &amp;&amp; cd meu-monorepo

npm init -y

npm install -D turbo typescript @types/node</code></pre>

<p>Agora crie a estrutura de diretórios:</p>

<pre><code>meu-monorepo/

├── packages/

│ ├── ui/

│ │ ├── package.json

│ │ ├── tsconfig.json

│ │ └── src/

│ ├── utils/

│ │ ├── package.json

│ │ ├── tsconfig.json

│ │ └── src/

│ └── api/

│ ├── package.json

│ ├── tsconfig.json

│ └── src/

├── turbo.json

├── tsconfig.json

├── package.json

└── pnpm-workspace.yaml (ou yarn.lock)</code></pre>

<p>A raiz do monorepo contém a configuração compartilhada, enquanto cada pacote dentro de <code>packages/</code> é um projeto independente com seu próprio <code>package.json</code> e <code>tsconfig.json</code>.</p>

<h3>Configurando o turbo.json</h3>

<p>O arquivo <code>turbo.json</code> define como o Turborepo executa suas tarefas. Este é o coração da orquestração:</p>

<pre><code class="language-json">{

&quot;$schema&quot;: &quot;https://turbo.build/schema.json&quot;,

&quot;globalDependencies&quot;: [&quot;tsconfig.json&quot;, &quot;.env&quot;],

&quot;pipeline&quot;: {

&quot;build&quot;: {

&quot;dependsOn&quot;: [&quot;^build&quot;],

&quot;outputs&quot;: [&quot;dist/**&quot;],

&quot;cache&quot;: true

},

&quot;lint&quot;: {

&quot;outputs&quot;: [],

&quot;cache&quot;: true

},

&quot;test&quot;: {

&quot;outputs&quot;: [&quot;coverage/**&quot;],

&quot;cache&quot;: false

},

&quot;dev&quot;: {

&quot;cache&quot;: false,

&quot;persistent&quot;: true

}

}

}</code></pre>

<p>A chave <code>dependsOn: [&quot;^build&quot;]</code> significa que uma tarefa <code>build</code> em um pacote só executa após o <code>build</code> completar em seus pacotes dependentes (indicado pelo <code>^</code>). Isso garante ordem correta de compilação. As <code>outputs</code> indicam quais arquivos gerados devem ser cacheados — o Turborepo armazena esses resultados e reutiliza em execuções subsequentes quando nada mudou.</p>

<h3>Configuração de TypeScript Compartilhada</h3>

<p>Na raiz, crie um <code>tsconfig.json</code> base:</p>

<pre><code class="language-json">{

&quot;compilerOptions&quot;: {

&quot;target&quot;: &quot;ES2020&quot;,

&quot;module&quot;: &quot;ESNext&quot;,

&quot;lib&quot;: [&quot;ES2020&quot;],

&quot;moduleResolution&quot;: &quot;node&quot;,

&quot;strict&quot;: true,

&quot;esModuleInterop&quot;: true,

&quot;skipLibCheck&quot;: true,

&quot;forceConsistentCasingInFileNames&quot;: true,

&quot;resolveJsonModule&quot;: true,

&quot;declaration&quot;: true,

&quot;declarationMap&quot;: true,

&quot;sourceMap&quot;: true

}

}</code></pre>

<p>Em <code>packages/utils/tsconfig.json</code>, estenda a configuração raiz:</p>

<pre><code class="language-json">{

&quot;extends&quot;: &quot;../../tsconfig.json&quot;,

&quot;compilerOptions&quot;: {

&quot;outDir&quot;: &quot;./dist&quot;,

&quot;rootDir&quot;: &quot;./src&quot;

},

&quot;include&quot;: [&quot;src/*/&quot;],

&quot;exclude&quot;: [&quot;node_modules&quot;, &quot;dist&quot;]

}</code></pre>

<h2>Estruturando Pacotes Interdependentes com TypeScript</h2>

<h3>Criando um Pacote de Utilitários</h3>

<p>Começamos com um pacote simples que será compartilhado entre outros. Em <code>packages/utils/package.json</code>:</p>

<pre><code class="language-json">{

&quot;name&quot;: &quot;@monorepo/utils&quot;,

&quot;version&quot;: &quot;1.0.0&quot;,

&quot;main&quot;: &quot;./dist/index.js&quot;,

&quot;types&quot;: &quot;./dist/index.d.ts&quot;,

&quot;scripts&quot;: {

&quot;build&quot;: &quot;tsc&quot;,

&quot;lint&quot;: &quot;eslint src/*/.ts&quot;

},

&quot;devDependencies&quot;: {

&quot;typescript&quot;: &quot;^5.0.0&quot;

}

}</code></pre>

<p>Crie <code>packages/utils/src/validators.ts</code>:</p>

<pre><code class="language-typescript">export interface ValidationResult {

valid: boolean;

errors: string[];

}

export function validateEmail(email: string): ValidationResult {

const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;

const valid = regex.test(email);

return {

valid,

errors: valid ? [] : [&#039;Email format is invalid&#039;]

};

}

export function validatePassword(password: string): ValidationResult {

const errors: string[] = [];

if (password.length &lt; 8) {

errors.push(&#039;Password must be at least 8 characters&#039;);

}

if (!/[A-Z]/.test(password)) {

errors.push(&#039;Password must contain uppercase letter&#039;);

}

if (!/[0-9]/.test(password)) {

errors.push(&#039;Password must contain a number&#039;);

}

return {

valid: errors.length === 0,

errors

};

}</code></pre>

<p>E o arquivo de entrada <code>packages/utils/src/index.ts</code>:</p>

<pre><code class="language-typescript">export * from &#039;./validators&#039;;

export { ValidationResult } from &#039;./validators&#039;;</code></pre>

<h3>Criando um Pacote que Depende de Outro</h3>

<p>Agora crie um pacote <code>api</code> que utilizará o pacote <code>utils</code>. Em <code>packages/api/package.json</code>:</p>

<pre><code class="language-json">{

&quot;name&quot;: &quot;@monorepo/api&quot;,

&quot;version&quot;: &quot;1.0.0&quot;,

&quot;main&quot;: &quot;./dist/index.js&quot;,

&quot;types&quot;: &quot;./dist/index.d.ts&quot;,

&quot;scripts&quot;: {

&quot;build&quot;: &quot;tsc&quot;,

&quot;dev&quot;: &quot;node -r ts-node/register src/server.ts&quot;,

&quot;lint&quot;: &quot;eslint src/*/.ts&quot;

},

&quot;dependencies&quot;: {

&quot;@monorepo/utils&quot;: &quot;*&quot;

},

&quot;devDependencies&quot;: {

&quot;typescript&quot;: &quot;^5.0.0&quot;,

&quot;ts-node&quot;: &quot;^10.0.0&quot;

}

}</code></pre>

<p>Crie <code>packages/api/src/user-service.ts</code>:</p>

<pre><code class="language-typescript">import { validateEmail, validatePassword, ValidationResult } from &#039;@monorepo/utils&#039;;

export interface User {

id: string;

email: string;

name: string;

}

export interface RegisterRequest {

email: string;

password: string;

name: string;

}

export class UserService {

private users: Map&lt;string, User&gt; = new Map();

private nextId: number = 1;

register(request: RegisterRequest): { success: boolean; user?: User; errors?: string[] } {

// Validar email

const emailValidation = validateEmail(request.email);

if (!emailValidation.valid) {

return { success: false, errors: emailValidation.errors };

}

// Validar password

const passwordValidation = validatePassword(request.password);

if (!passwordValidation.valid) {

return { success: false, errors: passwordValidation.errors };

}

// Criar usuário

const user: User = {

id: String(this.nextId++),

email: request.email,

name: request.name

};

this.users.set(user.id, user);

return { success: true, user };

}

getUser(id: string): User | undefined {

return this.users.get(id);

}

}</code></pre>

<p>Observe que o import é feito diretamente de <code>@monorepo/utils</code> graças à configuração do workspace npm/pnpm. O Turborepo garante que quando você compilar este pacote, o pacote <code>utils</code> já terá sido compilado.</p>

<h2>Otimizações, Caching e Execução em Paralelo</h2>

<h3>Compreendendo o Sistema de Cache</h3>

<p>O Turborepo calcula um hash para cada tarefa baseado em: arquivos de entrada, scripts, variáveis de ambiente e outputs anteriores. Se nada mudou, reutiliza o cache. Isso é transformador em CI/CD pipelines.</p>

<p>Configure em <code>turbo.json</code> para ser mais agressivo com cache:</p>

<pre><code class="language-json">{

&quot;pipeline&quot;: {

&quot;build&quot;: {

&quot;dependsOn&quot;: [&quot;^build&quot;],

&quot;outputs&quot;: [&quot;dist/&quot;, &quot;build/&quot;],

&quot;cache&quot;: true,

&quot;hashAlgorithm&quot;: &quot;sha256&quot;

},

&quot;test&quot;: {

&quot;dependsOn&quot;: [&quot;build&quot;],

&quot;outputs&quot;: [&quot;coverage/**&quot;],

&quot;cache&quot;: true

}

},

&quot;caching&quot;: {

&quot;outputLogs&quot;: &quot;errors-only&quot;

}

}</code></pre>

<p>Execute no seu monorepo:</p>

<pre><code class="language-bash">npx turbo run build

Na segunda execução (sem mudanças):

npx turbo run build

Verá: &quot;&gt;&gt;&gt; FULL TURBO [cached]&quot; — extremamente rápido</code></pre>

<h3>Executando Tarefas em Paralelo com Dependências</h3>

<p>Adicione um script no <code>package.json</code> raiz para execução completa:</p>

<pre><code class="language-json">{

&quot;scripts&quot;: {

&quot;build&quot;: &quot;turbo run build&quot;,

&quot;build:ui&quot;: &quot;turbo run build --filter=ui&quot;,

&quot;build:api&quot;: &quot;turbo run build --filter=api --filter=utils&quot;,

&quot;dev&quot;: &quot;turbo run dev --parallel&quot;,

&quot;lint&quot;: &quot;turbo run lint --parallel&quot;,

&quot;test&quot;: &quot;turbo run test --parallel&quot;

}

}</code></pre>

<p>O parâmetro <code>--parallel</code> executa tarefas que não têm dependências entre si simultaneamente. <code>--filter</code> permite limitar execução a pacotes específicos. Quando você roda <code>turbo run build</code>, o sistema:</p>

<ol>

<li>Analisa o grafo de dependências entre pacotes</li>

<li>Determina ordem de compilação (utils → api → ui)</li>

<li>Executa tarefas em paralelo sempre que possível</li>

<li>Reutiliza cache se disponível</li>

</ol>

<h3>Filtrando Execução para Desenvolvimento</h3>

<p>Para desenvolvimento local, frequentemente você quer rodar apenas pacotes modificados:</p>

<pre><code class="language-bash"># Execute dev apenas em pacotes que mudaram

turbo run dev --filter=&#039;...[origin/main]&#039;

Execute em um pacote e suas dependências

turbo run build --filter=@monorepo/api</code></pre>

<p>A sintaxe <code>...[origin/main]</code> (com GitDiff) roda tarefas apenas em pacotes que alteraram desde a branch main, economizando tempo em monorepos grandes.</p>

<h2>Conclusão</h2>

<p>Você aprendeu que o Turborepo transforma o desenvolvimento em monorepos através de três mecanismos essenciais: <strong>orquestração clara de dependências</strong> (definida em turbo.json), <strong>sistema de cache inteligente</strong> que reutiliza outputs quando inputs não mudam, e <strong>execução paralela respeitosa de dependências</strong> que compila apenas o necessário na ordem correta. Essas otimizações são críticas em equipes onde múltiplos pacotes compartilham código e precisam ser compilados juntos com frequência.</p>

<h2>Referências</h2>

<ul>

<li><a href="https://turbo.build/repo/docs" target="_blank" rel="noopener noreferrer">Turborepo Official Documentation</a></li>

<li><a href="https://www.typescriptlang.org/docs/handbook/project-references.html" target="_blank" rel="noopener noreferrer">TypeScript Monorepos Best Practices</a></li>

<li><a href="https://docs.npmjs.com/cli/v8/using-npm/workspaces" target="_blank" rel="noopener noreferrer">npm Workspaces Documentation</a></li>

<li><a href="https://turbo.build/repo/docs/core-concepts/caching" target="_blank" rel="noopener noreferrer">Turborepo Caching Deep Dive</a></li>

<li><a href="https://www.youtube.com/results?search_query=turbo+monorepo+typescript" target="_blank" rel="noopener noreferrer">Modern JavaScript Monorepo with Turbo and PNPM</a></li>

</ul>

<p>&lt;!-- FIM --&gt;</p>

Comentários

Mais em TypeScript

Dominando NestJS Avançado: Guards, Interceptors, Pipes e Exception Filters em Projetos Reais
Dominando NestJS Avançado: Guards, Interceptors, Pipes e Exception Filters em Projetos Reais

Introdução ao Middleware de NestJS NestJS é um framework robusto construído s...

O que Todo Dev Deve Saber sobre Mutation Testing com TypeScript: Stryker e Qualidade de Cobertura
O que Todo Dev Deve Saber sobre Mutation Testing com TypeScript: Stryker e Qualidade de Cobertura

Entendendo Mutation Testing: Além da Cobertura de Código Quando começamos a t...

O que Todo Dev Deve Saber sobre Inversão de Dependência com TypeScript: tsyringe e InversifyJS
O que Todo Dev Deve Saber sobre Inversão de Dependência com TypeScript: tsyringe e InversifyJS

Compreendendo Inversão de Dependência A inversão de dependência é um princípi...