
Struktur Folder Proyek Backend yang Scalable: Peluang dari Sistem Enterprise yang Saya Kerjakan
Struktur folder backend yang scalable bukan soal estetika. Ini pengalaman saya merapikan proyek enterprise Node.js dari 200 file jadi arsitektur modular yang enak dirawat.
Tim saya pernah mewarisi proyek backend Node.js dengan 200+ file TypeScript yang 80% di antaranya menumpuk di satu folder src/. Semua handler, query Prisma, helper, dan cron job aduk-aduk jadi satu. Setiap kali onboarding engineer baru, butuh dua minggu sebelum mereka berani menyentuh kode. Dua minggu itu mahal, dan masalahnya bukan kualitas kode — tapi struktur folder yang tidak mengikuti pertumbuhan sistem.
Setelah melewati tiga kali restuktur besar di proyek enterprise multi-tenant, saya punya pendapat yang cukup kuat soal ini: struktur folder adalah keputusan arsitektur, sama seriusnya dengan pemilihan database. Dan keputusan paling mahal biasanya bukan struktur yang salah sejak awal, tapi struktur yang tidak pernah diubah saat aplikasi tumbuh.
Kesalahan yang Paling Sering Saya Lihat: Folder Berdasarkan Jenis File
Banyak template starter pakai pola ini:
src/
controllers/
services/
repositories/
models/
utils/
Pola ini dikenal sebagai layered by kind, dan untuk proyek kecil (< 30 file) sebenarnya tidak masalah. Tapi di sistem enterprise, pola ini cepat berubah jadi bencana. Untuk mengubah satu fitur "invoice", Anda harus buka controllers/invoice.ts, services/invoice.ts, repositories/invoice.ts, plus utils/invoiceCalc.ts. Satu perubahan bisnis tersebar di empat folder.
Masalah kedua: tidak ada batas yang jelas antar fitur. Service invoice dengan mudah meng-import service user, yang meng-import paymentService, yang balik lagi ke invoice — dependency graph yang lama-lama jadi spaghetti yang tidak bisa dipotong.

Struktur yang Akhirnya Saya Pakai: Modular by Feature
Setelah dua kali gagal refactor, struktur yang bertahan di produksi untuk sistem multi-tenant dengan 15+ engineer adalah modular by feature:
src/
modules/
auth/
auth.routes.ts
auth.controller.ts
auth.service.ts
auth.repository.ts
auth.schema.ts # Zod/DTO
auth.test.ts
__mocks__/
invoice/
invoice.routes.ts
invoice.controller.ts
invoice.service.ts
invoice.repository.ts
invoice.events.ts # publisher/subscriber
tenant/
...
shared/
db/ # Prisma client, transaction helper
logger/
errors/ # AppError, error codes
queue/ # koneksi BullMQ
app.ts
server.ts
Prinsip dasarnya satu: semua yang berubah bersama, diletakkan bersama. Kalau product manager minta perubahan logika invoice, 95% diff harus berada dalam satu folder. PR jadi kecil, code review jadi cepat, dan git log -- src/modules/invoice langsung memberi riwayat fitur itu.
Kalau Anda belum terbiasa dengan pola pemisahan seperti ini, best practice REST API design yang pernah saya tulis cocok dibaca dulu karena struktur folder dan desain endpoint saling mengikat.
Aturan Import: Kunci yang Sering Dilupakan
Struktur folder tidak akan berguna tanpa aturan dependency. Yang saya pakai:
modules/*boleh meng-importshared/*— boleh.modules/authboleh meng-importmodules/tenantHANYA lewat index publik (modules/tenant/index.ts).shared/*dilarang keras meng-importmodules/*.
Untuk memaksa aturan ini, saya pakai eslint-plugin-import dengan no-restricted-imports:
{
"rules": {
"no-restricted-imports": ["error", {
"patterns": [{
"group": ["**/shared/**"],
"message": "shared/ tidak boleh import dari modules/"
}]
}]
}
}
Tanpa linter, aturan arsitektur hanya ada di kepala senior engineer — dan lupa begitu orang baru masuk.
Kapan Layer Terpisah Masih Masuk Akal
Saya tidak mau terdengar seperti salesman modular. Ada kasus di mana shared/ membesar sendiri — misalnya logger, koneksi database, dan helper queue jadi proyek mini. Di satu proyek besar, kami akhirnya mengekstrak shared/ jadi private package npm terpisah (@perusahaan/db, @perusahaan/logger). Itu membantu karena konsumen package-nya bukan cuma satu service.
Tapi untuk monolit satu service, jangan buru-buru bikin monorepo. Ekstraksi package itu punya overhead tersendiri (versi, publish pipeline, sinkronisasi release). Kalau service Anda cuma satu dan timnya di bawah 20 orang, folder shared/ biasa sudah cukup.
Begitu tim tumbuh dan domain antar modul makin berbeda kebutuhannya, struktur modular ini juga jadi batu pijakan alami menuju microservices — tiap folder modules/* adalah kandidat ekstraksi. Prinsip serupa juga saya bahas di strategi refactoring legacy code tanpa menghentikan bisnis, karena merestrukturisasi folder pada sistem hidup pada dasarnya adalah refactor, bukan rewrite.
Detail Kecil yang Berdampak Besar
Beberapa keputusan kecil yang terbukti berpengaruh di proyek nyata:
File masuk ke queue, bukan blok event loop
Modul besar pasti punya task berat — export laporan, kirim email massal. Jangan pernah taruh logika ini di controller. Kami konsisten menaruhnya di *.events.ts atau dedicated worker folder, dengan BullMQ untuk background job dan queue yang berjalan di proses terpisah dari HTTP server. Struktur folder yang memisahkan worker/ dari api/ membuat deployment scaling-nya juga jelas: naikkan replika API tanpa menaikkan worker, atau sebaliknya.
apps/
api/ # HTTP server
worker/ # BullMQ worker
packages/
modules/ # domain logic, dipakai api & worker
Validasi tinggal di pintu masuk
Setiap modul punya *.schema.ts sendiri yang mendefinisikan bentuk input. Ini konsisten dengan prinsip never trust the client di validasi input backend — schema hidup berdampingan dengan modul yang memakainya, bukan terkumpul di satu folder validations/ raksasa.
Naming yang membosankan itu bagus
Saya standarkan nama file: <nama>.routes.ts, <nama>.service.ts, <nama>.repository.ts. Tidak kreatif? Justru itu intinya. Konvensi yang membosankan membuat orang bisa menebak lokasi file tanpa membaca dokumentasi. Ctrl+P + "invoice.service" selalu mengarah ke satu file yang benar.

Proses Migrasi Tanpa Drama
Kalau Anda sudah punya proyek dengan struktur berantakan, jangan refactor sekaligus. Urutan yang saya pakai:
- Bekukan aturan baru — semua file baru wajib masuk struktur modular, file lama dibiarkan.
- Pindahkan modul paling sering berubah duluan — ini yang memberi ROI tercepat.
- Gunakan
git mvsupaya riwayat file terjaga dan blame tetap akurat. - Pindah bertahap per modul, satu PR per modul, maksimal 300 baris diff per PR.
Di proyek terakhir, migrasi penuh 200 file memakan enam minggu tanpa menghentikan feature delivery. Kuncinya bukan cepat, tapi tiap langkah selalu deployable.
Perlu diakui: ada trade-off nyata. Struktur modular duplikasi sedikit kode antar modul (misalnya dua modul punya helper pagination mirip), dan untuk tim kecil 2-3 orang, overhead konsistensi ini bisa terasa berlebihan. Kalau proyek Anda diperkirakan selesai dalam 3 bulan dan tidak akan disentuh lagi — layered sederhana masih sah.
Takeaway yang Bisa Anda Pakai Hari Ini
- Kalau diff satu perubahan fitur selalu menyentuh 4+ folder, itu sinyal struktur Anda sudah tidak scalable — bukan sinyal developer Anda kurang disiplin.
- Mulailah dengan
modules/+shared/, paksa boundary pakai ESLint, jangan pakai kepercayaan. - Standarkan naming file sampai membosankan; itu fitur, bukan bug.
- Pisahkan
api/danworker/sejak awal meskipun masih satu repo — Anda akan berterima kasih saat scaling pertama kali. - Restrukturisasi itu proyek bertahap, bukan big-bang rewrite.
Struktur folder yang baik tidak membuat aplikasi Anda cepat secara ajaib, tapi ia menentukan seberapa cepat tim Anda bisa berubah — dan di jangka panjang, itu yang paling mahal.
Tomi Hartanto
Senior Software Engineer
Backend developer dengan 8+ tahun pengalaman membangun sistem scalable. Menulis tentang arsitektur backend, database, dan DevOps.
Artikel Terkait

N+1 Query Problem di Prisma dan Laravel Eloquent: Studi Kasus dan Cara Saya Memperbaikinya

Background Job dan Queue untuk Task Berat: Konsep dan Implementasi dengan BullMQ

Validasi Input di Backend: Kenapa Never Trust the Client Bukan Sekadar Slogan
Tomi Hartanto
Senior Software EngineerBackend developer dengan 8+ tahun pengalaman membangun sistem scalable. Menulis tentang arsitektur backend, database, dan DevOps.