Header Ads

Struktur Folder Nuxt 3 untuk Project Besar (Best Practice 2026)

Struktur Folder Nuxt 3 untuk Project Besar (Best Practice 2026) - Kalau kamu pakai Nuxt 3 untuk project kecil, biasanya struktur default sudah cukup. Tapi begitu aplikasi mulai membesar (banyak fitur, tim bertambah, API makin banyak), struktur folder yang asal-asalan akan bikin:

  • Developer bingung cari file
  • Logic campur aduk di page
  • Bug sulit dilacak
  • Refactor makin mahal

Di artikel ini saya bagikan struktur folder Nuxt 3 yang rapi untuk project besar (best practice 2026), lengkap dengan pola composables, services, stores, dan standar penamaan file.

🎯 Tujuan Struktur Folder yang “Bener”

Struktur folder bukan soal “gaya”, tapi soal maintainability.

  • Scalable: fitur baru masuk tanpa merusak struktur
  • Readable: orang baru join cepat paham
  • Separation of concerns: UI, logic, dan API tidak campur
  • Testable: service & composable mudah diuji

📦 Struktur Folder Rekomendasi untuk Project Besar

Ini struktur yang saya rekomendasikan untuk Nuxt 3 yang sudah “serius”:


/ (root)
├─ app.vue
├─ nuxt.config.ts
├─ package.json
├─ .env
├─ public/
├─ server/
│  ├─ api/
│  ├─ middleware/
│  ├─ utils/
│  └─ plugins/
└─ src/
   ├─ assets/
   ├─ components/
   │  ├─ base/
   │  ├─ ui/
   │  └─ feature/
   ├─ composables/
   ├─ layouts/
   ├─ middleware/
   ├─ pages/
   ├─ plugins/
   ├─ services/
   ├─ stores/
   ├─ types/
   └─ utils/

Kenapa saya pisahkan ke folder src/?
Karena project besar akan punya banyak folder, dan src/ membantu rapihin “application code” supaya root tetap bersih.

Catatan: Nuxt 3 mendukung srcDir. Tambahkan ini di nuxt.config.ts:


export default defineNuxtConfig({
  srcDir: 'src/',
})

🧱 Penjelasan Tiap Folder (yang paling penting)

1) pages/ = Routing + Page Composition

Prinsipnya: pages jangan jadi “tempat semua logic numpuk”. Pages cukup:

  • ambil data dari composable/store
  • susun komponen
  • set metadata halaman (SEO)

Contoh page yang “bersih”:


<script setup>
definePageMeta({ middleware: ['auth'] })

useHead({
  title: 'Dashboard',
  meta: [{ name: 'description', content: 'Dashboard Nuxt 3' }],
})

const { items, loading, fetchItems } = useDashboard()
onMounted(fetchItems)
</script>

<template>
  <div>
    <h1>Dashboard</h1>
    <DashboardTable :items="items" :loading="loading" />
  </div>
</template>

2) components/ = UI Reusable

Untuk project besar, saya sarankan pola ini:

  • components/base/ → komponen super generic (Button, Input, Modal)
  • components/ui/ → komponen UI reusable tapi lebih “app-specific” (DataTable, Pagination)
  • components/feature/ → komponen spesifik fitur (UserForm, OrderList)

Contoh:


components/
├─ base/
│  ├─ BaseButton.vue
│  ├─ BaseInput.vue
│  └─ BaseModal.vue
├─ ui/
│  ├─ DataTable.vue
│  ├─ Pagination.vue
│  └─ LoadingSpinner.vue
└─ feature/
   └─ users/
      ├─ UserForm.vue
      ├─ UserCard.vue
      └─ UserTable.vue

3) composables/ = Logic Reusable (Bukan UI)

Composable adalah tempat logic yang dipakai lintas page/komponen: fetch data, pagination, state lokal, dan side-effect.

Contoh: useUsers yang fokus pada domain “users”.


// src/composables/useUsers.js
import { ref } from 'vue'
import { usersService } from '@/services/usersService'

export function useUsers() {
  const users = ref([])
  const loading = ref(false)
  const error = ref(null)

  const fetchUsers = async (params = {}) => {
    loading.value = true
    error.value = null
    try {
      users.value = await usersService.list(params)
    } catch (e) {
      error.value = e
    } finally {
      loading.value = false
    }
  }

  return { users, loading, error, fetchUsers }
}

Rule penting: composable jangan langsung “tau” soal tampilan (misalnya toast UI khusus). Itu tugas layer UI atau plugin.


4) services/ = Tempat API Call

Ini bagian yang sering dilupakan. Banyak orang API call langsung di page/composable. Untuk project besar, pisahkan ke services.

Contoh service:


// src/services/api.js
export const api = {
  async get(url) {
    return await $fetch(url, { method: 'GET' })
  },
  async post(url, body) {
    return await $fetch(url, { method: 'POST', body })
  },
}

// src/services/usersService.js
import { api } from '@/services/api'

export const usersService = {
  async list(params) {
    return await api.get('/api/users?' + new URLSearchParams(params))
  },
  async create(payload) {
    return await api.post('/api/users', payload)
  },
}

Dengan pola ini, besok kalau ganti base URL, ganti auth header, atau pindah backend—kamu ubah di satu tempat.


5) stores/ (Pinia) = Global State

Kalau data dipakai lintas banyak page dan harus konsisten (misal user login, role, permission), taruh di store.


// src/stores/authStore.js
import { defineStore } from 'pinia'

export const useAuthStore = defineStore('auth', {
  state: () => ({
    user: null,
    roles: [],
  }),
  getters: {
    isLoggedIn: (state) => !!state.user,
  },
  actions: {
    setUser(user) {
      this.user = user
    },
    logout() {
      this.user = null
      this.roles = []
    },
  },
})

Rule simpel:

  • Composable = logic
  • Store = global state

6) middleware/ = Guard & Route Rules

Untuk project besar, middleware harus rapi. Pisahkan berdasarkan fungsi:


middleware/
├─ auth.js
└─ guest.js

// src/middleware/auth.js
export default defineNuxtRouteMiddleware(() => {
  const token = useCookie('auth_token')
  if (!token.value) return navigateTo('/login')
})

7) server/ (Nitro) = Backend Tipis / API

Kalau kamu pakai server route Nuxt (Nitro), rapikan:


server/
├─ api/
│  ├─ users.get.js
│  ├─ users.post.js
│  └─ auth/
│     └─ login.post.js
├─ middleware/
└─ utils/

Dengan struktur ini, backend tipis tetap maintainable.


📌 Konvensi Penamaan (biar tim rapi)

  • Components: PascalCase → UserTable.vue
  • Composables: camelCase dengan prefix use → useUsers.js
  • Stores: camelCase + Store → authStore.js
  • Services: camelCase + Service → usersService.js
  • Utils: fungsi kecil → formatCurrency.js

📊 “Do & Don’t” untuk Project Besar

Area Do ✅ Don’t ❌
Pages Tipis, fokus susun UI + call composable/store Taruh semua logic fetch & transform data di page
API Call Pisahkan ke services Fetch random di banyak tempat
State Global di Pinia, lokal di composable Semua dimasukin store
Components Base/UI/Feature terpisah Semua ditumpuk 1 folder tanpa grouping

⚠️ Error Umum yang Sering Terjadi (dan Solusinya)

1) “Project makin besar, tapi folder makin berantakan”

Solusi: mulai grouping components/feature, pisahkan services dan composables, lalu konsisten naming.

2) “Logic fetch data tersebar, susah tracing bug”

Solusi: buat satu service per domain (users, orders, products). Semua API call lewat service.

3) “Store terlalu gemuk”

Solusi: store hanya global state. Logic yang reusable taruh composable. Helper taruh utils.


✅ Checklist Implementasi (biar kamu tinggal eksekusi)

  • Gunakan srcDir agar root bersih
  • Pages tipis, logic pindah ke composables/services
  • Buat folder components/base, components/ui, components/feature
  • API call hanya lewat services/
  • Global state lewat Pinia (stores)
  • Middleware dipisah (auth/guest/role)

🔗 Baca Juga (Internal Link Seri Nuxt Authority)


Kesimpulan

Struktur folder yang benar akan menentukan apakah project Nuxt 3 kamu bisa berkembang dengan nyaman atau malah jadi “legacy” dalam beberapa bulan.

Kalau kamu ingin Nuxt 3 yang scalable, kuncinya adalah:

  • Pages fokus UI
  • Logic di composables
  • API di services
  • State global di stores

Dengan pola ini, project kamu lebih rapi, mudah dimaintain, dan siap dikerjakan tim.


FAQ

Q: Harus pakai src/ gak?

A: Tidak wajib, tapi sangat membantu untuk project besar agar root tidak penuh.

Q: Kapan pakai composable dan kapan pakai store?

A: Composable untuk logic reusable, store untuk state global lintas halaman.

Q: Apakah server route Nuxt cocok untuk production?

A: Cocok untuk backend tipis. Kalau bisnis logic berat, bisa tetap pisahkan backend khusus.

No comments

Powered by Blogger.