Next.js course Β· Module 9: Integrations and Advanced Features

Monorepo z Turborepo - Skalowanie projektow Next.js

5 min read
In this lesson8

W rozleglej infrastrukturze Metropolii Quantum 2150, gdzie dziesiaki mikroserwisow i aplikacji frontendowych wspolpracuja ze soba, potrzebujesz narzedzia do zarzadzania kodem na wielka skale. Turborepo to system budowania dla monorepo, ktory dziala z Next.js jak precyzyjny kwantowy procesor - szybko, wydajnie i niezawodnie.

Dlaczego monorepo dla Next.js?

Monorepo to strategia przechowywania wielu projektow w jednym repozytorium. Dla ekosystemu Next.js daje to ogromne korzysci:

  • Wspoldzielenie kodu - komponenty UI, konfiguracje, typy uzywane w wielu aplikacjach
  • Atomowe zmiany - zmiana w wspoldzielonej bibliotece natychmiast widoczna wszedzie
  • Spojny tooling - jedna konfiguracja ESLint, TypeScript, Prettier
  • Uproszczone CI/CD - jeden pipeline dla calego ekosystemu
  • Lepsza wspolpraca - wszystko w jednym miejscu, latwy code review

Konfiguracja Turborepo

Inicjalizacja monorepo

1# Utworz nowe monorepo z Turborepo
2npx create-turbo@latest quantum-monorepo
3
4# Lub dodaj Turborepo do istniejacego projektu
5npm install turbo --save-dev

Struktura katalogow

1quantum-monorepo/
2  apps/
3    web/                 # Glowna aplikacja Next.js
4      package.json
5      next.config.js
6    admin/               # Panel admina Next.js
7      package.json
8      next.config.js
9    docs/                # Dokumentacja Next.js
10      package.json
11
12  packages/
13    ui/                  # Wspoldzielone komponenty React
14      package.json
15      src/
16        Button.tsx
17        Card.tsx
18        index.ts
19    config-eslint/       # Wspoldzielona konfiguracja ESLint
20      package.json
21    config-typescript/   # Wspoldzielona konfiguracja TypeScript
22      package.json
23    types/               # Wspoldzielone interfejsy TypeScript
24      package.json
25      src/
26        user.ts
27        product.ts
28
29  turbo.json             # Konfiguracja Turborepo
30  package.json           # Root package.json

Konfiguracja turbo.json

1{
2  "$schema": "https://turbo.build/schema.json",
3  "globalDependencies": ["**/.env.*local"],
4  "pipeline": {
5    "build": {
6      "dependsOn": ["^build"],
7      "outputs": [".next/**", "!.next/cache/**", "dist/**"]
8    },
9    "lint": {
10      "dependsOn": ["^build"]
11    },
12    "dev": {
13      "cache": false,
14      "persistent": true
15    },
16    "test": {
17      "dependsOn": ["^build"]
18    },
19    "type-check": {
20      "dependsOn": ["^build"]
21    }
22  }
23}

Kluczowe elementy konfiguracji:

  • dependsOn: ["^build"] - buduj zaleznosci zanim zbudujesz pakiet
  • outputs - co cachowac (foldery wynikowe)
  • cache: false - dev server nie powinien byc cachowany
  • persistent: true - procesy dlugotrwale (dev server)

Root package.json

1{
2  "name": "quantum-monorepo",
3  "private": true,
4  "workspaces": ["apps/*", "packages/*"],
5  "scripts": {
6    "build": "turbo build",
7    "dev": "turbo dev",
8    "lint": "turbo lint",
9    "test": "turbo test",
10    "type-check": "turbo type-check"
11  },
12  "devDependencies": {
13    "turbo": "^2.0.0"
14  }
15}

Wspoldzielone pakiety

Pakiet UI - komponenty React

1// packages/ui/package.json
2{
3  "name": "@quantum/ui",
4  "version": "0.1.0",
5  "main": "./src/index.ts",
6  "types": "./src/index.ts",
7  "dependencies": {
8    "react": "^19.2.0"
9  },
10  "devDependencies": {
11    "@quantum/config-typescript": "workspace:*",
12    "typescript": "^5.5.0"
13  }
14}
1// packages/ui/src/Button.tsx
2import React from 'react';
3
4interface ButtonProps {
5  children: React.ReactNode;
6  variant?: 'primary' | 'secondary' | 'danger';
7  onClick?: () => void;
8  disabled?: boolean;
9}
10
11export function Button({ children, variant = 'primary', onClick, disabled }: ButtonProps) {
12  const styles = {
13    primary: { background: '#7c4dff', color: '#fff' },
14    secondary: { background: '#64ffda', color: '#000' },
15    danger: { background: '#f44336', color: '#fff' },
16  };
17
18  return (
19    <button
20      onClick={onClick}
21      disabled={disabled}
22      style={{
23        ...styles[variant],
24        padding: '10px 24px',
25        borderRadius: '8px',
26        border: 'none',
27        cursor: disabled ? 'not-allowed' : 'pointer',
28        opacity: disabled ? 0.5 : 1,
29      }}
30    >
31      {children}
32    </button>
33  );
34}
1// packages/ui/src/index.ts
2export { Button } from './Button';
3export { Card } from './Card';

Pakiet types - wspoldzielone interfejsy

1// packages/types/src/user.ts
2export interface User {
3  id: string;
4  email: string;
5  name: string;
6  role: 'admin' | 'user' | 'moderator';
7  createdAt: string;
8}
1// packages/types/src/index.ts
2export * from './user';
3export * from './product';

Workspace dependencies

Aby uzyc wspoldzielonego pakietu w aplikacji Next.js:

1// apps/web/package.json
2{
3  "name": "@quantum/web",
4  "dependencies": {
5    "@quantum/ui": "workspace:*",
6    "@quantum/types": "workspace:*",
7    "next": "^16.0.0",
8    "react": "^19.2.0",
9    "react-dom": "^19.2.0"
10  },
11  "devDependencies": {
12    "@quantum/config-typescript": "workspace:*",
13    "@quantum/config-eslint": "workspace:*"
14  }
15}

Uzycie w kodzie aplikacji:

1// apps/web/app/page.tsx
2import { Button, Card } from '@quantum/ui';
3import type { Product } from '@quantum/types';
4
5export default async function HomePage() {
6  return (
7    <div>
8      <h1>Quantum Store</h1>
9      <Button variant="primary">Dodaj do koszyka</Button>
10    </div>
11  );
12}

Cache i pipeline Turborepo

Turborepo uzywa inteligentnego systemu cache, ktory zapamietuje wyniki budowania i pomija niezmienione pakiety.

Jak dziala cache

1# Pierwsze budowanie - wszystko od zera
2npx turbo build
3# Output: 5 tasks, 0 cached, 5 not cached
4# Czas: 45s
5
6# Drugie budowanie (bez zmian) - pelny cache
7npx turbo build
8# Output: 5 tasks, 5 cached, 0 not cached
9# Czas: 0.3s

Remote Caching z Vercel

1# Polacz z Vercel Remote Cache
2npx turbo login
3npx turbo link
4
5# Teraz cache jest wspoldzielony miedzy developerami i CI

Filtrowanie budowania

1# Zbuduj tylko konkretna aplikacje
2npx turbo build --filter=@quantum/web
3
4# Zbuduj aplikacje i jej zaleznosci
5npx turbo build --filter=@quantum/web...
6
7# Zbuduj tylko to co sie zmienilo od ostatniego commita
8npx turbo build --filter=[HEAD^1]

Deploying z monorepo na Vercel

Konfiguracja next.config.js dla monorepo

1// apps/web/next.config.js
2/** @type {import('next').NextConfig} */
3const nextConfig = {
4  transpilePackages: ['@quantum/ui'],
5  output: 'standalone',
6};
7
8module.exports = nextConfig;

Opcja transpilePackages mowi Next.js, zeby transpilowac kod z pakietow monorepo (domyslnie Next.js nie transpiluje node_modules).

Wieloaplikacyjny deploy

Vercel automatycznie buduje tylko zmienione aplikacje. Jesli zmienisz packages/ui, aplikacje zalezne od tego pakietu zostana przebudowane, a te niezalezne - pominiete.

Najlepsze praktyki

1. Konwencje nazewnictwa pakietow

Uzywaj scope (@nazwa-organizacji/) dla wszystkich pakietow i aplikacji:

1@quantum/ui
2@quantum/types
3@quantum/config-eslint
4@quantum/web
5@quantum/admin

2. Wersjonowanie

Uzywaj workspace:* dla wewnetrznych zaleznosci - nigdy nie hardcoduj wersji:

1"@quantum/ui": "workspace:*"

3. Zarzadzanie zmiennymi srodowiskowymi

Kazda aplikacja ma wlasny plik .env, a turbo.json deklaruje globalne zaleznosci od plikow srodowiskowych.

Summary

Turborepo to potezne narzedzie do skalowania projektow Next.js w architekturze monorepo:

  1. Wspoldzielone pakiety - UI, typy, konfiguracje uzywane w wielu aplikacjach
  2. Inteligentny cache - pomijanie niezmienionych pakietow, przyspieszenie buildow
  3. Pipeline - zaleznosci miedzy zadaniami, rownolegle budowanie
  4. Workspace dependencies - workspace:* dla wewnetrznych pakietow
  5. Remote caching - wspoldzielony cache miedzy developerami i CI
  6. Deploy na Vercel - natywne wsparcie, automatyczne wykrywanie zmian

W Metropolii Quantum 2150, gdzie kazdy projekt rosnie i wymaga skalowania, Turborepo jest fundamentem architektury - pozwala zarzadzac dziesiatkami aplikacji i bibliotek z jednego miejsca.

Code for this lesson: App.tsx
1import React, { useState } from 'react';
2
3interface Package {
4  name: string;
5  type: 'app' | 'package';
6  status: 'cached' | 'building' | 'built' | 'pending';
7  duration?: number;
8  deps: string[];
9}
10
11const TurborepoDemo = () => {
12  const [packages, setPackages] = useState<Package[]>([
13    { name: '@quantum/web', type: 'app', status: 'pending', deps: ['@quantum/ui', '@quantum/types'] },
14    { name: '@quantum/admin', type: 'app', status: 'pending', deps: ['@quantum/ui', '@quantum/types'] },
15    { name: '@quantum/docs', type: 'app', status: 'pending', deps: [] },
16    { name: '@quantum/ui', type: 'package', status: 'pending', deps: ['@quantum/types'] },
17    { name: '@quantum/types', type: 'package', status: 'pending', deps: [] },
18    { name: '@quantum/config-ts', type: 'package', status: 'pending', deps: [] },
19    { name: '@quantum/config-eslint', type: 'package', status: 'pending', deps: [] },
20  ]);
21  const [running, setRunning] = useState(false);
22  const [logs, setLogs] = useState<string[]>([]);
23  const [cacheEnabled, setCacheEnabled] = useState(true);
24  const [buildCount, setBuildCount] = useState(0);
25  const [changedPkg, setChangedPkg] = useState<string | null>(null);
26
27  const addLog = (msg: string) => setLogs(prev => [...prev.slice(-12), msg]);
28  const sleep = (ms: number) => new Promise(r => setTimeout(r, ms));
29
30  const statusIcon = (s: string) => {
31    switch (s) {
32      case 'built': case 'cached': return 'βœ…';
33      case 'building': return '⏳';
34      default: return 'βšͺ';
35    }
36  };
37
38  const statusColor = (s: string) => {
39    switch (s) {
40      case 'built': return '#4caf50';
41      case 'cached': return '#64ffda';
42      case 'building': return '#ff9800';
43      default: return '#555';
44    }
45  };
46
47  const runBuild = async () => {
48    setRunning(true);
49    setLogs([]);
50    setPackages(prev => prev.map(p => ({ ...p, status: 'pending', duration: undefined })));
51    setBuildCount(c => c + 1);
52    const isSecondBuild = buildCount > 0;
53
54    addLog('$ npx turbo build' + (changedPkg ? ' --filter=' + changedPkg + '...' : ''));
55
56    // Build packages first (no deps first)
57    const noDeps = packages.filter(p => p.type === 'package' && p.deps.length === 0);
58    for (const pkg of noDeps) {
59      const isCached = isSecondBuild && cacheEnabled && pkg.name !== changedPkg;
60      setPackages(prev => prev.map(p => p.name === pkg.name ? { ...p, status: 'building' } : p));
61      addLog((isCached ? 'CACHE HIT ' : 'Building ') + pkg.name);
62      await sleep(isCached ? 100 : 300);
63      setPackages(prev => prev.map(p => p.name === pkg.name ? { ...p, status: isCached ? 'cached' : 'built', duration: isCached ? 0 : 2 } : p));
64    }
65
66    // Then packages with deps
67    const withDeps = packages.filter(p => p.type === 'package' && p.deps.length > 0);
68    for (const pkg of withDeps) {
69      const isCached = isSecondBuild && cacheEnabled && pkg.name !== changedPkg && !pkg.deps.includes(changedPkg || '');
70      setPackages(prev => prev.map(p => p.name === pkg.name ? { ...p, status: 'building' } : p));
71      addLog((isCached ? 'CACHE HIT ' : 'Building ') + pkg.name);
72      await sleep(isCached ? 100 : 400);
73      setPackages(prev => prev.map(p => p.name === pkg.name ? { ...p, status: isCached ? 'cached' : 'built', duration: isCached ? 0 : 5 } : p));
74    }
75
76    // Then apps
77    const apps = packages.filter(p => p.type === 'app');
78    for (const pkg of apps) {
79      const affectedByChange = changedPkg && (pkg.deps.includes(changedPkg) || pkg.name === changedPkg);
80      const isCached = isSecondBuild && cacheEnabled && !affectedByChange;
81      if (changedPkg && !affectedByChange && pkg.name !== changedPkg) {
82        setPackages(prev => prev.map(p => p.name === pkg.name ? { ...p, status: 'cached', duration: 0 } : p));
83        addLog('SKIP ' + pkg.name + ' (not affected)');
84        await sleep(80);
85        continue;
86      }
87      setPackages(prev => prev.map(p => p.name === pkg.name ? { ...p, status: 'building' } : p));
88      addLog((isCached ? 'CACHE HIT ' : 'Building ') + pkg.name);
89      await sleep(isCached ? 150 : 600);
90      setPackages(prev => prev.map(p => p.name === pkg.name ? { ...p, status: isCached ? 'cached' : 'built', duration: isCached ? 0 : 15 } : p));
91    }
92
93    const cached = packages.filter(() => true).length; // placeholder
94    addLog('Build complete!');
95    setRunning(false);
96    setChangedPkg(null);
97  };
98
99  const simulateChange = (pkgName: string) => {
100    setChangedPkg(pkgName);
101    addLog('Changed: ' + pkgName);
102  };
103
104  const reset = () => {
105    setPackages(prev => prev.map(p => ({ ...p, status: 'pending', duration: undefined })));
106    setLogs([]);
107    setBuildCount(0);
108    setChangedPkg(null);
109  };
110
111  return (
112    <div style={{ background: '#0f0f23', minHeight: '100vh', padding: '20px', color: '#fff', fontFamily: 'sans-serif' }}>
113      <h1 style={{ color: '#64ffda', textAlign: 'center' }}>Monorepo with Turborepo</h1>
114      <p style={{ textAlign: 'center', color: '#888', marginBottom: '20px' }}>Shared packages + Intelligent caching + Pipeline</p>
115
116      <div style={{ maxWidth: '900px', margin: '0 auto' }}>
117        <div style={{ display: 'flex', gap: '8px', justifyContent: 'center', marginBottom: '16px', flexWrap: 'wrap' }}>
118          <button onClick={runBuild} disabled={running}
119            style={{ padding: '8px 20px', background: running ? '#555' : '#4caf50', border: 'none', borderRadius: '6px', color: '#fff', cursor: running ? 'default' : 'pointer', fontWeight: 'bold' }}>
120            {running ? 'Building...' : 'turbo build'}
121          </button>
122          <button onClick={reset} disabled={running}
123            style={{ padding: '8px 20px', background: 'rgba(255,255,255,0.1)', border: 'none', borderRadius: '6px', color: '#fff', cursor: 'pointer' }}>
124            Reset
125          </button>
126          <label style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
127            <input type="checkbox" checked={cacheEnabled} onChange={() => setCacheEnabled(!cacheEnabled)} />
128            <span style={{ color: cacheEnabled ? '#64ffda' : '#666', fontSize: '13px' }}>Remote Cache</span>
129          </label>
130        </div>
131
132        {buildCount > 0 && !running && (
133          <div style={{ display: 'flex', gap: '6px', justifyContent: 'center', marginBottom: '12px', flexWrap: 'wrap' }}>
134            <span style={{ color: '#888', fontSize: '12px', padding: '4px 0' }}>Simulate change in:</span>
135            {packages.filter(p => p.type === 'package').map(p => (
136              <button key={p.name} onClick={() => simulateChange(p.name)}
137                style={{ padding: '4px 10px', background: changedPkg === p.name ? '#ff9800' : 'rgba(255,255,255,0.1)', border: 'none', borderRadius: '4px', color: '#fff', cursor: 'pointer', fontSize: '11px' }}>
138                {p.name.replace('@quantum/', '')}
139              </button>
140            ))}
141          </div>
142        )}
143
144        <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '16px' }}>
145          <div>
146            <h3 style={{ color: '#7c4dff', marginBottom: '10px' }}>Workspace Packages</h3>
147            <div style={{ background: 'rgba(255,255,255,0.05)', borderRadius: '10px', overflow: 'hidden' }}>
148              {packages.map(pkg => (
149                <div key={pkg.name} style={{ display: 'flex', alignItems: 'center', gap: '8px', padding: '10px 12px', borderBottom: '1px solid #1a1a2e' }}>
150                  <span>{statusIcon(pkg.status)}</span>
151                  <span style={{ background: pkg.type === 'app' ? '#7c4dff33' : '#64ffda33', color: pkg.type === 'app' ? '#7c4dff' : '#64ffda', padding: '2px 6px', borderRadius: '4px', fontSize: '10px' }}>
152                    {pkg.type}
153                  </span>
154                  <span style={{ flex: 1, color: statusColor(pkg.status), fontSize: '13px' }}>{pkg.name}</span>
155                  {pkg.status === 'cached' && <span style={{ color: '#64ffda', fontSize: '10px' }}>CACHED</span>}
156                  {pkg.duration !== undefined && pkg.duration > 0 && <span style={{ color: '#555', fontSize: '11px' }}>{pkg.duration}s</span>}
157                </div>
158              ))}
159            </div>
160
161            <div style={{ marginTop: '12px', background: 'rgba(255,255,255,0.05)', padding: '12px', borderRadius: '8px' }}>
162              <h4 style={{ color: '#64ffda', fontSize: '12px', marginBottom: '8px' }}>Dependency Graph</h4>
163              <div style={{ fontSize: '11px', fontFamily: 'monospace', color: '#888' }}>
164                <div>@quantum/web ── @quantum/ui ── @quantum/types</div>
165                <div>@quantum/admin ── @quantum/ui ── @quantum/types</div>
166                <div>@quantum/docs (no internal deps)</div>
167              </div>
168            </div>
169          </div>
170
171          <div>
172            <h3 style={{ color: '#7c4dff', marginBottom: '10px' }}>Build Log</h3>
173            <div style={{ background: '#000', padding: '12px', borderRadius: '8px', minHeight: '300px', maxHeight: '400px', overflowY: 'auto', fontFamily: 'monospace', fontSize: '11px' }}>
174              {logs.length === 0 ? (
175                <span style={{ color: '#555' }}>Click "turbo build" to start...</span>
176              ) : logs.map((log, i) => (
177                <div key={i} style={{ color: log.includes('CACHE') ? '#64ffda' : log.includes('SKIP') ? '#888' : log.includes('Building') ? '#ff9800' : '#0f0', padding: '2px 0' }}>
178                  {log}
179                </div>
180              ))}
181            </div>
182          </div>
183        </div>
184      </div>
185    </div>
186  );
187};
188
189export default TurborepoDemo;

Spotted a mistake in this lesson?

Check yourself

Answer the questions from this lesson. Pick an answer to see right away whether it is correct.

  1. 1. Which statement best describes the main advantage of Turborepo in Next.js projects?

Hands-on tasks in the game

  • Vertical ordering

    Arrange the order of building packages in a Turborepo monorepo (from packages without dependencies to applications)

Useful articles