Struktur Proyek TypeScript untuk Aplikasi Dunia Nyata: Apa yang Benar-Benar Berhasil

Foto oleh Unsplash

Foto oleh Unsplash
Setiap tutorial TypeScript menunjukkan struktur awal yang sama: src/components, src/pages, mungkin src/utils. Ini berfungsi baik untuk aplikasi todo. Lalu Anda memulai proyek nyata dengan autentikasi, billing, API eksternal, banyak peran pengguna, dan sistem desain — dan struktur datar itu mulai runtuh dalam minggu-minggu pertama.
Prinsip inti: kelompokkan berdasarkan batas fitur, bukan berdasarkan tipe file. Menempatkan semua hooks di /hooks, semua tipe di /types, dan semua komponen di /components terdengar terorganisir — sampai Anda mengerjakan fitur dan perlu melompat antara enam direktori untuk kode terkait.
Keputusan organisasi yang paling berdampak adalah memisahkan components/ui (sistem desain — Button, Input, Modal, Card, Badge) dari components/features (komposit khusus domain — UserCard, InvoiceTable, ApprovalBadge). Komponen UI bersifat bodoh: mereka menerima props, merender markup, tidak ada pengambilan data, tidak ada logika bisnis.
Direktori services/ untuk semua panggilan API eksternal adalah pola yang saya adopsi dari NestJS dan bawa ke pekerjaan Next.js saya. Setiap file layanan bertanggung jawab atas satu batas eksternal: auth.ts untuk autentikasi, billing.ts untuk API pembayaran. Layanan adalah fungsi async biasa — tidak ada React, tidak ada hooks, tidak ada JSX.
src/
├── app/ # Next.js App Router pages
│ ├── [locale]/
│ │ ├── (pages)/
│ │ │ ├── dashboard/
│ │ │ └── settings/
│ │ └── layout.tsx
│ └── api/ # Route handlers
│ └── v1/
├── components/ # UI components (dumb, presentational)
│ ├── ui/ # Design system atoms (Button, Input…)
│ └── features/ # Domain-specific composites
├── lib/ # Pure utilities (no React deps)
│ ├── utils.ts
│ └── validators.ts
├── services/ # External API calls, data fetching
│ ├── auth.ts
│ └── billing.ts
├── hooks/ # Custom React hooks
├── stores/ # Zustand / Jotai state
├── types/ # Shared TypeScript types & interfaces
│ ├── api.ts
│ └── domain.ts
└── constants/ # App-wide constants, enumsTambahkan file barrel index.ts ke setiap direktori fitur, tetapi TIDAK ke components/ui. File barrel untuk sistem desain menciptakan risiko ketergantungan sirkular dan memperlambat language server TypeScript pada proyek besar. Biarkan impor komponen UI tetap eksplisit.
Pengaturan default TypeScript terlalu permisif untuk kode produksi. Mengaktifkan strict true adalah hal minimal, tetapi ada tiga opsi tambahan yang menangkap kelas bug yang signifikan: noUncheckedIndexedAccess, exactOptionalPropertyTypes, dan noImplicitReturns.
Bahaya paling besar any dalam codebase TypeScript adalah yang tersembunyi di dalam respons API. response.json() mengembalikan any, dan jika Anda tidak mempersempitnya, janji keamanan tipe seluruh aplikasi Anda adalah kebohongan. Saya menggunakan pendekatan dua lapisan: skema Zod untuk validasi runtime dan antarmuka TypeScript yang berasal dari skema via z.infer.
// tsconfig.json — production-grade config
{
"compilerOptions": {
"target": "ES2022",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true, // never turn this off
"noUncheckedIndexedAccess": true, // catch arr[i] undefined
"exactOptionalPropertyTypes": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"moduleResolution": "bundler",
"paths": {
"@/*": ["./src/*"],
"@/components/*": ["./src/components/*"],
"@/lib/*": ["./src/lib/*"],
"@/types/*": ["./src/types/*"],
"@/services/*": ["./src/services/*"]
}
}
}
// types/domain.ts — co-locate domain types
export interface User {
readonly id: string
email: string
role: UserRole
createdAt: Date
}
export type UserRole = "admin" | "member" | "viewer"
// Never use 'any' — use 'unknown' and narrow:
function parseApiResponse(raw: unknown): User {
if (!isUser(raw)) throw new Error("Invalid user shape")
return raw
}
function isUser(val: unknown): val is User {
return (
typeof val === "object" && val !== null &&
"id" in val && typeof (val as User).id === "string"
)
}eslint-plugin-import dengan aturan no-restricted-imports dapat menegakkan batasan arsitektur secara otomatis. Cegah services/ mengimpor dari components/, cegah lib/ mengimpor React — aturan-aturan ini menangkap pelanggaran pelapisan pada waktu lint.
Terlalu banyak menggunakan file barrel (index.ts re-export di mana-mana) adalah salah satu penyebab paling umum perlambatan language server TypeScript dan kesalahan ketergantungan sirkular dalam proyek besar. Jika IDE Anda terasa lambat, hapus file barrel dari jalur yang sering diakses dan gunakan impor langsung.
Untuk proyek yang berkembang menjadi backend terpisah atau aplikasi mobile, monorepo Turborepo menjadi berharga. Aturan praktis: jika dua aplikasi berbeda perlu berbagi tipe atau komponen, ekstrak mereka ke paket. Jika hanya satu aplikasi, tetap di proyek Next.js tunggal.
Ukuran akhir dari setiap struktur folder adalah manusia: bisakah developer yang belum pernah melihat codebase menemukan file yang tepat dalam 60 detik? Jika jawabannya tidak, strukturnya terlalu cerdas. Simpan kode fitur bersama, pisahkan sistem desain, jaga utilitas tetap murni.