Web Development

Nuxt.js: Vue Full-Stack Framework

Tutorial lengkap Nuxt.js dari nol β€” SSR, SSG, auto-imports, modules, server API, routing otomatis, dan deployment dengan contoh kode praktis

1. Pengenalan Nuxt.js

Nuxt.js adalah framework full-stack berbasis Vue.js yang menyediakan arsitektur terstruktur untuk membangun aplikasi web modern. Dikembangkan oleh tim Nuxt Labs, framework ini menghilangkan konfigurasi manual yang rumit dan menyediakan fitur-fitur bawaan seperti SSR, SSG, auto-imports, dan routing otomatis.

Nuxt 3 (versi stabil saat ini) dibangun di atas Vue 3, Vite, dan Nitro (server engine), menjadikannya salah satu framework full-stack paling lengkap di ekosistem JavaScript.

Mengapa Memilih Nuxt.js?

Keunggulan Penjelasan
Zero ConfigurationRouting, bundling, dan optimasi bekerja out-of-the-box tanpa setup manual
SSR & SSGDukungan server-side rendering dan static generation dalam satu framework
Auto-ImportsKomponen, composables, dan utils di-import otomatis tanpa deklarasi manual
Server Engine (Nitro)Server API, middleware server, dan deploy ke berbagai platform cloud
SEO-FriendlyMeta tags, sitemap, dan prerendering built-in untuk SEO optimal
Ekosistem ModulesRatusan modul siap pakai untuk auth, CMS, analytics, UI, dan lainnya

Nuxt.js vs Alternatif Lain

Aspek Nuxt.js Next.js Astro
Base FrameworkVue.jsReactMulti (Vue, React, Svelte)
SSRβœ… Ya (Nitro)βœ… Yaβœ… Partial
SSGβœ… Yaβœ… Yaβœ… Ya (utama)
File-based Routingβœ… Otomatisβœ… Otomatisβœ… Otomatis
Server APIβœ… Nitroβœ… API Routesβœ… Endpoint
Learning Curve🟒 Mudah🟑 Sedang🟒 Mudah
Diagram: Arsitektur Nuxt.js 3
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                    NUXT APPLICATION                      β”‚
β”‚                                                         β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚  β”‚                  Vue 3 + Composition API          β”‚  β”‚
β”‚  β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚  β”‚
β”‚  β”‚  β”‚ Pages   β”‚  β”‚Componentsβ”‚  β”‚  Composables     β”‚  β”‚  β”‚
β”‚  β”‚  β”‚(routing)β”‚  β”‚(reusable)β”‚  β”‚(useFetch, useStateβ”‚  β”‚  β”‚
β”‚  β”‚  β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚  β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β”‚          β”‚                                               β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚  β”‚              Nuxt Core (Vite + Nitro)             β”‚  β”‚
β”‚  β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚  β”‚
β”‚  β”‚  β”‚  Vite    β”‚  β”‚  Nitro    β”‚  β”‚ Nuxt Modules  β”‚  β”‚  β”‚
β”‚  β”‚  β”‚ (bundler)β”‚  β”‚ (server)  β”‚  β”‚  (plugins)    β”‚  β”‚  β”‚
β”‚  β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚  β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β”‚                                                         β”‚
β”‚  Output:  SSR (server)  β”‚  SSG (static)  β”‚  CSR (SPA)   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

2. Instalasi dan Setup

Untuk membuat proyek Nuxt baru, Anda bisa menggunakan beberapa package manager. Nuxt menggunakan nuxi sebagai CLI resminya.

Membuat Proyek Baru

Bash
# Membuat proyek Nuxt baru dengan npx
npx nuxi@latest init nuxt-app

# Atau dengan pnpm
pnpm dlx nuxi@latest init nuxt-app

# Masuk ke direktori
cd nuxt-app

# Install dependencies
npm install

# Jalankan development server
npm run dev

# Output:
#   Nuxt 3.x.x with Nitro
#   ➜ Local:   http://localhost:3000/

Struktur Proyek Nuxt

File Structure
nuxt-app/
β”œβ”€β”€ .nuxt/                ← Generated (auto)
β”œβ”€β”€ assets/               ← CSS, fonts, images (processed)
β”œβ”€β”€ components/           ← Vue components (auto-imported)
β”‚   β”œβ”€β”€ AppHeader.vue
β”‚   β”œβ”€β”€ AppFooter.vue
β”‚   └── UserCard.vue
β”œβ”€β”€ composables/          ← Composables (auto-imported)
β”‚   └── useAuth.js
β”œβ”€β”€ layouts/              ← Layout components
β”‚   └── default.vue
β”œβ”€β”€ middleware/           ← Route middleware
β”‚   └── auth.js
β”œβ”€β”€ pages/                ← File-based routing
β”‚   β”œβ”€β”€ index.vue         ← /
β”‚   β”œβ”€β”€ about.vue         ← /about
β”‚   └── blog/
β”‚       β”œβ”€β”€ index.vue     ← /blog
β”‚       └── [id].vue      ← /blog/:id
β”œβ”€β”€ plugins/              ← Vue plugins
β”œβ”€β”€ public/               ← Static files (tidak diproses)
β”‚   └── favicon.ico
β”œβ”€β”€ server/               ← Server-side code
β”‚   β”œβ”€β”€ api/
β”‚   β”‚   └── hello.js      ← /api/hello
β”‚   β”œβ”€β”€ middleware/
β”‚   └── utils/
β”œβ”€β”€ app.vue               ← Root component
β”œβ”€β”€ nuxt.config.ts        ← Konfigurasi Nuxt
β”œβ”€β”€ package.json
└── tsconfig.json

Konfigurasi Dasar

nuxt.config.ts
// nuxt.config.ts
export default defineNuxtConfig({
  // Meta tags global untuk SEO
  app: {
    head: {
      title: 'BeebaneLabs - Tutorial Web Development',
      meta: [
        { charset: 'utf-8' },
        { name: 'viewport', content: 'width=device-width, initial-scale=1' },
        { name: 'description', content: 'Tutorial web development modern' }
      ],
      link: [
        { rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }
      ]
    }
  },

  // CSS global
  css: ['~/assets/css/main.css'],

  // Modul yang digunakan
  modules: [
    '@pinia/nuxt',
    '@nuxtjs/tailwindcss',
    '@nuxt/image'
  ],

  // Konfigurasi development
  devtools: { enabled: true }
})
πŸ’‘ Tips

File nuxt.config.ts mendukung TypeScript secara native. Gunakan defineNuxtConfig() untuk mendapatkan type hints dan autocompletion yang akurat di editor Anda.

3. Routing Otomatis

Salah satu fitur terbaik Nuxt adalah file-based routing. Setiap file .vue di folder pages/ secara otomatis menjadi route. Tidak perlu konfigurasi router manual seperti di Vue Router standalone.

Struktur Routing

File Structure β†’ Routes
pages/
β”œβ”€β”€ index.vue              β†’  /
β”œβ”€β”€ about.vue              β†’  /about
β”œβ”€β”€ contact.vue            β†’  /contact
β”œβ”€β”€ blog/
β”‚   β”œβ”€β”€ index.vue          β†’  /blog
β”‚   β”œβ”€β”€ [slug].vue         β†’  /blog/:slug  (dynamic)
β”‚   └── [...slug].vue      β†’  /blog/*      (catch-all)
β”œβ”€β”€ user/
β”‚   β”œβ”€β”€ [id].vue           β†’  /user/:id
β”‚   └── [id]/
β”‚       β”œβ”€β”€ index.vue      β†’  /user/:id
β”‚       β”œβ”€β”€ posts.vue      β†’  /user/:id/posts
β”‚       └── settings.vue   β†’  /user/:id/settings
└── products/
    └── [[category]].vue   β†’  /products atau /products/:category
                                (opsional parameter)

Dynamic Routes dengan Parameter

pages/blog/[slug].vue
<template>
  <div class="blog-post">
    <h1>{{ post.title }}</h1>
    <p class="meta">Ditulis oleh {{ post.author }} Β· {{ post.date }}</p>
    <div class="content" v-html="post.content" />
  </div>
</template>

<script setup>
// useRoute() untuk mengakses parameter URL
const route = useRoute()
const slug = route.params.slug

// useFetch() β€” composable bawaan Nuxt untuk data fetching
const { data: post, error } = await useFetch(`/api/posts/${slug}`)

// Jika post tidak ditemukan, tampilkan error 404
if (error.value) {
  throw createError({
    statusCode: 404,
    statusMessage: 'Artikel tidak ditemukan'
  })
}

// Dynamic meta tags untuk SEO
useHead({
  title: post.value?.title,
  meta: [
    { name: 'description', content: post.value?.excerpt }
  ]
})
</script>

Navigasi dengan NuxtLink

Vue β€” NuxtLink
<template>
  <nav>
    <!-- NuxtLink menggantikan <a> tag, memberikan SPA navigation -->
    <NuxtLink to="/">Beranda</NuxtLink>
    <NuxtLink to="/blog">Blog</NuxtLink>
    <NuxtLink to="/about">Tentang</NuxtLink>

    <!-- Dynamic link -->
    <NuxtLink :to="`/blog/${post.slug}`">
      {{ post.title }}
    </NuxtLink>

    <!-- External link -->
    <NuxtLink to="https://github.com" external>
      GitHub
    </NuxtLink>

    <!-- Prefetching (default aktif) -->
    <NuxtLink to="/products" no-prefetch>
      Produk (tanpa prefetch)
    </NuxtLink>
  </nav>
</template>

Nested Routes dengan NuxtPage

pages/user/[id].vue β€” Parent Layout
<template>
  <div class="user-profile">
    <h1>Profil: {{ user.name }}</h1>

    <nav class="user-tabs">
      <NuxtLink :to="`/user/${user.id}`">Overview</NuxtLink>
      <NuxtLink :to="`/user/${user.id}/posts`">Postingan</NuxtLink>
      <NuxtLink :to="`/user/${user.id}/settings`">Pengaturan</NuxtLink>
    </nav>

    <!-- NuxtPage merender child routes di sini -->
    <NuxtPage />
  </div>
</template>

<script setup>
const route = useRoute()
const { data: user } = await useFetch(`/api/users/${route.params.id}`)
</script>
Diagram: File-Based Routing Flow
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚              NUXT FILE-BASED ROUTING                 β”‚
β”‚                                                     β”‚
β”‚  pages/about.vue ──────→  /about                    β”‚
β”‚  pages/blog/index.vue ──→  /blog                    β”‚
β”‚  pages/blog/[id].vue ───→  /blog/123  (dynamic)    β”‚
β”‚  pages/user/[id]/       β”‚                           β”‚
β”‚    β”œβ”€β”€ index.vue ───────→  /user/42                 β”‚
β”‚    └── posts.vue ───────→  /user/42/posts           β”‚
β”‚                                                     β”‚
β”‚  Nuxt otomatis generate Vue Router config           β”‚
β”‚  dari struktur folder pages/                        β”‚
β”‚                                                     β”‚
β”‚  pages/ β†’ .nuxt/router.js (generated)               β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

4. Server-Side Rendering (SSR)

SSR adalah mode default Nuxt. Setiap request halaman di-render di server terlebih dahulu, lalu dikirim ke browser sebagai HTML lengkap. Ini sangat penting untuk SEO dan performa initial load.

Cara Kerja SSR di Nuxt

Penjelasan SSR
1. Browser mengirim request β†’ GET /blog/my-post
2. Server (Nitro) menerima request
3. Nuxt menjalankan lifecycle:
   a. Middleware server (server/middleware/)
   b. Data fetching (useFetch, useAsyncData)
   c. Render komponen Vue menjadi HTML string
4. HTML + data dikirim ke browser
5. Vue "menghidupkan" (hydrate) HTML di browser
6. Setelah hydration, aplikasi berjalan sebagai SPA

Keuntungan:
- Search engine bisa meng-crawl konten lengkap
- First Contentful Paint (FCP) lebih cepat
- Meta tags ter-render di server

Data Fetching di SSR

composables/useProducts.js
// useFetch β€” composable bawaan Nuxt untuk SSR-safe fetching
// Otomatis: dijalankan di server saat SSR, di client saat navigasi SPA

// Contoh 1: useFetch sederhana
const { data: products, pending, error } = await useFetch('/api/products', {
  query: { category: 'electronics', limit: 10 },
  // Cache key β€” data di-cache berdasarkan key unik
  key: 'products-electronics',
  // Transform data sebelum digunakan
  transform: (response) => response.items,
  // Re-fetch setiap 60 detik
  getCachedData: (key, nuxtApp) => {
    return nuxtApp.payload.data[key] || null
  }
})

// Contoh 2: useAsyncData untuk logic kompleks
const { data: userProfile } = await useAsyncData('user-profile', async () => {
  // Bisa melakukan multiple fetch atau logic kompleks
  const [user, posts, stats] = await Promise.all([
    $fetch('/api/users/me'),
    $fetch('/api/users/me/posts'),
    $fetch('/api/users/me/stats')
  ])

  return { user, posts, stats }
}, {
  // Hanya jalankan di server (tidak di client)
  server: true,
  // Lazy: jangan block rendering, fetch di background
  lazy: false
})

// Contoh 3: Lazy fetching (tidak block rendering)
const { data: comments } = await useLazyFetch(`/api/posts/${id}/comments`)
// UI langsung dirender, comments muncul setelah fetch selesai
⚠️ Peringatan SSR

Hati-hati dengan akses window, document, atau localStorage di komponen yang di-SSR. Objek ini tidak tersedia di server. Gunakan onMounted() atau if (process.client) untuk mengakses browser API.

Vue β€” Browser API Safety
<script setup>
// ❌ Salah β€” akan error di server
const width = window.innerWidth

// βœ… Benar β€” gunakan onMounted
onMounted(() => {
  const width = window.innerWidth
  console.log('Window width:', width)
})

// βœ… Atau gunakan useNuxtApp().$isServer
const nuxtApp = useNuxtApp()
if (!nuxtApp.isServer) {
  const token = localStorage.getItem('token')
}

// βœ… Composable yang aman untuk SSR
const isClient = ref(false)
onMounted(() => { isClient.value = true })
</script>

5. Static Site Generation (SSG)

SSG memungkinkan Anda meng-generate halaman statis HTML saat build time. Ini cocok untuk blog, dokumentasi, dan landing page yang kontennya jarang berubah. Hasilnya bisa di-host di CDN manapun tanpa perlu server Node.js.

Mengaktifkan SSG

nuxt.config.ts
// nuxt.config.ts β€” konfigurasi untuk SSG
export default defineNuxtConfig({
  // Route prerendering β€” generate halaman statis
  nitro: {
    prerender: {
      routes: [
        '/',
        '/about',
        '/blog',
        '/pricing'
      ]
    }
  },

  // Atau set seluruh app sebagai static
  // Jalankan: npx nuxi generate
  // Ini akan meng-generate semua halaman sebagai HTML statis
})

// Jalankan perintah untuk generate static
// npm run generate
// Hasil: .output/public/ β€” siap di-deploy ke CDN

Dynamic Routes dengan SSG

pages/blog/[slug].vue β€” Prerendering Dynamic Pages
<script setup>
// Untuk dynamic routes, Nuxt perlu tahu semua slug yang tersedia
// agar bisa generate halaman statis untuk setiap slug

// Method 1: Define routes via nuxt.config.ts
// nitro.prerender.routes: ['/blog/post-1', '/blog/post-2']

// Method 2: Gunakan sitemap module
// @nuxtjs/sitemap bisa auto-generate routes dari API

// Fetch data β€” saat SSG, ini dijalankan di build time
const route = useRoute()
const { data: post } = await useFetch(`/api/posts/${route.params.slug}`, {
  key: `post-${route.params.slug}`
})

// Data di-embed ke HTML statis
// Saat user mengunjungi halaman, data langsung tersedia tanpa fetch ulang
</script>

Hybrid Rendering

nuxt.config.ts β€” Hybrid
// Nuxt mendukung hybrid rendering!
// Beberapa halaman SSR, beberapa SSG, beberapa SPA

export default defineNuxtConfig({
  routeRules: {
    // Halaman beranda β€” SSR (selalu fresh)
    '/': { prerender: true },

    // Blog β€” SSG (diprerender saat build)
    '/blog/**': { prerender: true },

    // Dashboard β€” SPA (client-side only, perlu auth)
    '/dashboard/**': { ssr: false },

    // API β€” ISR (revalidate setiap 60 detik)
    '/api/products/**': { swr: 60 },

    // Halaman statis β€” cache selamanya
    '/about': { static: true },

    // Redirect
    '/old-page': { redirect: '/new-page' }
  }
})
πŸ’‘ Tips

Hybrid rendering adalah fitur powerful Nuxt yang memungkinkan Anda memilih mode rendering per-route. Ini berarti Anda bisa memiliki blog yang di-SSG, dashboard yang SPA, dan halaman produk yang SSR β€” semua dalam satu aplikasi!

6. Auto-Imports

Nuxt secara otomatis meng-import komponen, composables, dan helper functions. Anda tidak perlu menulis import statement secara manual β€” cukup gunakan langsung di template atau script.

Apa yang Di-auto-import?

Penjelasan Auto-Imports
1. KOMPONEN (dari folder components/)
   File: components/UserCard.vue
   β†’ Langsung pakai: <UserCard />
   β†’ Nested: components/blog/PostCard.vue β†’ <BlogPostCard />

2. COMPOSABLES (dari folder composables/)
   File: composables/useAuth.js
   β†’ Langsung pakai: const { user, login } = useAuth()

3. UTILS (dari folder utils/)
   File: utils/format.js β†’ export function formatRupiah() {...}
   β†’ Langsung pakai: formatRupiah(150000)

4. VUE APIs (built-in)
   ref(), computed(), watch(), onMounted(), etc.
   β†’ Tidak perlu: import { ref } from 'vue'

5. NUXT APIs (built-in)
   useFetch(), useRoute(), useState(), navigateTo(), etc.
   β†’ Tidak perlu: import { useFetch } from '#app'

Komponen Auto-Imported

components/UserCard.vue
<template>
  <div class="user-card">
    <img :src="user.avatar" :alt="user.name" />
    <h3>{{ user.name }}</h3>
    <p>{{ user.email }}</p>
    <span :class="['badge', user.role]">{{ user.role }}</span>
  </div>
</template>

<script setup>
// Props dengan defineProps β€” juga auto-imported
const props = defineProps({
  user: {
    type: Object,
    required: true
  }
})
</script>

<style scoped>
.user-card {
  padding: 1.5rem;
  border-radius: 12px;
  background: var(--card-bg);
  border: 1px solid var(--border-color);
}

.badge {
  padding: 4px 12px;
  border-radius: 999px;
  font-size: 0.75rem;
  font-weight: 600;
}

.badge.admin { background: #ef4444; color: white; }
.badge.member { background: #3b82f6; color: white; }
</style>
pages/users.vue β€” Menggunakan Auto-Imported Component
<template>
  <div class="users-page">
    <h1>Daftar Pengguna</h1>

    <!-- Tidak perlu import UserCard β€” langsung pakai! -->
    <div class="users-grid">
      <UserCard
        v-for="user in users"
        :key="user.id"
        :user="user"
      />
    </div>
  </div>
</template>

<script setup>
// Tidak perlu import ref, useFetch β€” auto-imported!
const { data: users } = await useFetch('/api/users')
</script>

Lazy Components

Vue β€” Lazy Components
<template>
  <div>
    <!-- Komponen berat yang jarang dilihat -->
    <!-- Prefix LazyLoad untuk lazy loading -->
    <LazyHeavyChart v-if="showChart" :data="chartData" />

    <!-- Komponen modal yang jarang muncul -->
    <LazyUserModal v-model="showModal" />

    <!-- Komponen di bawah fold -->
    <LazyBlogComments :post-id="postId" />
  </div>
</template>

<script setup>
const showChart = ref(false)
const showModal = ref(false)
const postId = useRoute().params.id
</script>

7. Composables dan Helpers

Composables adalah fungsi yang menggunakan Vue Composition API untuk menyimpan dan berbagi stateful logic. Nuxt membuat composables sangat mudah digunakan berkat auto-imports.

Membuat Composable

composables/useCounter.js
// composables/useCounter.js
// Auto-imported β€” langsung pakai useCounter() di mana saja

export function useCounter(initialValue = 0) {
  const count = ref(initialValue)

  function increment() { count.value++ }
  function decrement() { count.value-- }
  function reset() { count.value = initialValue }

  // Computed
  const isPositive = computed(() => count.value > 0)
  const isZero = computed(() => count.value === 0)

  return {
    count: readonly(count),  // readonly agar tidak bisa diubah dari luar
    increment,
    decrement,
    reset,
    isPositive,
    isZero
  }
}

Composable dengan Server State

composables/useAuth.js
// composables/useAuth.js
export function useAuth() {
  // useState β€” shared state yang SSR-safe
  // Key harus unik untuk menghindari conflict
  const user = useState('auth-user', () => null)
  const token = useState('auth-token', () => null)

  const isAuthenticated = computed(() => !!user.value)
  const isAdmin = computed(() => user.value?.role === 'admin')

  async function login(email, password) {
    try {
      const response = await $fetch('/api/auth/login', {
        method: 'POST',
        body: { email, password }
      })

      user.value = response.user
      token.value = response.token

      // Simpan token di cookie (SSR-safe)
      const cookieToken = useCookie('auth_token', {
        maxAge: 60 * 60 * 24 * 7, // 7 hari
        httpOnly: false,
        secure: true,
        sameSite: 'lax'
      })
      cookieToken.value = response.token

      return { success: true }
    } catch (error) {
      return { success: false, error: error.data?.message }
    }
  }

  async function logout() {
    user.value = null
    token.value = null

    // Hapus cookie
    const cookieToken = useCookie('auth_token')
    cookieToken.value = null

    await navigateTo('/login')
  }

  async function fetchUser() {
    if (!token.value) return

    try {
      const data = await $fetch('/api/auth/me', {
        headers: { Authorization: `Bearer ${token.value}` }
      })
      user.value = data.user
    } catch {
      await logout()
    }
  }

  return {
    user: readonly(user),
    isAuthenticated,
    isAdmin,
    login,
    logout,
    fetchUser
  }
}

Built-in Nuxt Composables

Composable Kegunaan
useFetch()Data fetching SSR-safe dengan caching
useAsyncData()Data fetching kompleks dengan custom logic
useRoute()Akses informasi route saat ini
useRouter()Programmatic navigation
useState()SSR-safe reactive state (shared)
useHead()Dynamic head/meta tags
useCookie()SSR-safe cookie access
useError()Akses error yang sedang aktif
useRequestHeaders()Akses headers dari incoming request
useRuntimeConfig()Akses runtime configuration

8. Server API Routes

Nuxt memiliki Nitro server engine yang memungkinkan Anda membuat API endpoints langsung di dalam proyek Nuxt. File di folder server/api/ secara otomatis menjadi API routes.

Membuat API Route

server/api/hello.js
// server/api/hello.js
// Otomatis tersedia di: GET /api/hello

export default defineEventHandler((event) => {
  return {
    message: 'Halo dari Nuxt Server API!',
    timestamp: new Date().toISOString()
  }
})

API dengan Query dan Body

server/api/products.js
// server/api/products.js
// GET /api/products?category=electronics&limit=10

export default defineEventHandler(async (event) => {
  // Mengambil query parameters
  const query = getQuery(event)
  const { category, limit = 20, page = 1 } = query

  // Fetch dari database atau external API
  const products = await db.products.findMany({
    where: category ? { category } : {},
    take: parseInt(limit),
    skip: (parseInt(page) - 1) * parseInt(limit),
    orderBy: { createdAt: 'desc' }
  })

  const total = await db.products.count({
    where: category ? { category } : {}
  })

  return {
    products,
    pagination: {
      page: parseInt(page),
      limit: parseInt(limit),
      total,
      totalPages: Math.ceil(total / parseInt(limit))
    }
  }
})
server/api/products.post.js
// server/api/products.post.js
// Suffix .post = hanya menerima POST request

export default defineEventHandler(async (event) => {
  // Validasi body request
  const body = await readBody(event)

  if (!body.name || !body.price) {
    throw createError({
      statusCode: 400,
      statusMessage: 'Name dan price wajib diisi'
    })
  }

  // Simpan ke database
  const product = await db.products.create({
    data: {
      name: body.name,
      price: parseFloat(body.price),
      category: body.category || 'uncategorized',
      description: body.description || ''
    }
  })

  return { success: true, product }
})

Server Middleware

server/middleware/auth.js
// server/middleware/auth.js
// Middleware ini berjalan untuk SEMUA server routes

export default defineEventHandler((event) => {
  // Hanya untuk API routes yang dimulai dengan /api/protected
  const path = getRequestURL(event).pathname

  if (path.startsWith('/api/protected')) {
    const token = getHeader(event, 'Authorization')

    if (!token) {
      throw createError({
        statusCode: 401,
        statusMessage: 'Token tidak ditemukan'
      })
    }

    // Verifikasi token dan attach user ke event context
    try {
      const user = verifyToken(token.replace('Bearer ', ''))
      event.context.user = user
    } catch {
      throw createError({
        statusCode: 401,
        statusMessage: 'Token tidak valid'
      })
    }
  }
})

Server Utils

server/utils/db.js
// server/utils/db.js
// Auto-imported di semua server files

import { PrismaClient } from '@prisma/client'

// Singleton pattern β€” satu instance Prisma
let prisma

export function usePrisma() {
  if (!prisma) {
    prisma = new PrismaClient()
  }
  return prisma
}

// Contoh penggunaan di server API:
// server/api/users/[id].js
// const db = usePrisma()
// const user = await db.user.findUnique({ where: { id: params.id } })

9. Nuxt Modules

Nuxt Modules adalah plugin yang memperluas fungsionalitas Nuxt. Ada ratusan modul komunitas yang bisa Anda gunakan, mulai dari UI framework hingga CMS dan analytics.

Menggunakan Modules

nuxt.config.ts
// nuxt.config.ts
export default defineNuxtConfig({
  modules: [
    // UI & Styling
    '@nuxtjs/tailwindcss',
    '@nuxtjs/color-mode',
    '@nuxtjs/google-fonts',

    // State Management
    '@pinia/nuxt',

    // Images & Media
    '@nuxt/image',

    // SEO & Analytics
    '@nuxtjs/sitemap',
    '@nuxtjs/robots',
    'nuxt-gtag',

    // Authentication
    '@sidebase/nuxt-auth',

    // Content
    '@nuxt/content',
  ],

  // Konfigurasi per-module
  tailwindcss: {
    cssPath: '~/assets/css/tailwind.css',
    configPath: 'tailwind.config.js'
  },

  colorMode: {
    classSuffix: '',
    preference: 'dark',
    fallback: 'dark'
  },

  image: {
    quality: 80,
    format: ['webp', 'jpg']
  },

  sitemap: {
    hostname: 'https://beebanelabs.pages.dev'
  }
})

Modul Populer

Modul Fungsi Install
@nuxtjs/tailwindcssTailwind CSS integrationnpm i @nuxtjs/tailwindcss
@pinia/nuxtState management Pinianpm i @pinia/nuxt
@nuxt/imageAuto image optimizationnpm i @nuxt/image
@nuxt/contentMarkdown/CMS contentnpm i @nuxt/content
@nuxtjs/sitemapAuto sitemap generationnpm i @nuxtjs/sitemap
nuxt-iconIcon library (Iconify)npm i nuxt-icon
@vueuse/nuxtVueUse composablesnpm i @vueuse/nuxt
@nuxtjs/color-modeDark/light mode togglenpm i @nuxtjs/color-mode

Membuat Custom Module

modules/logger.js
// modules/logger.js
// Custom Nuxt module β€” auto-loaded dari folder modules/

import { defineNuxtModule, createResolver, addServerHandler } from '@nuxt/kit'

export default defineNuxtModule({
  meta: {
    name: 'logger',
    configKey: 'logger'
  },

  defaults: {
    enabled: true,
    level: 'info'
  },

  setup(options, nuxt) {
    if (!options.enabled) return

    const resolver = createResolver(import.meta.url)

    // Register server middleware
    addServerHandler({
      handler: resolver.resolve('./runtime/logger-middleware'),
      middleware: true
    })

    console.log(`[Logger] Module aktif β€” level: ${options.level}`)
  }
})

10. Middleware dan Layouts

Middleware di Nuxt berjalan sebelum halaman di-render, cocok untuk autentikasi, redirect, atau logging. Layouts membungkus halaman dengan struktur UI yang konsisten seperti header, sidebar, dan footer.

Route Middleware

middleware/auth.js
// middleware/auth.js
// Middleware bernama β€” dipanggil manual di setiap halaman

export default defineNuxtRouteMiddleware((to, from) => {
  const { isAuthenticated } = useAuth()

  if (!isAuthenticated.value) {
    // Redirect ke login jika belum autentikasi
    return navigateTo('/login', {
      // Simpan halaman tujuan untuk redirect setelah login
      query: { redirect: to.fullPath }
    })
  }
})
middleware/log.global.js
// middleware/log.global.js
// Suffix .global = berjalan di SEMUA halaman (tidak perlu manual)

export default defineNuxtRouteMiddleware((to, from) => {
  console.log(`[Route] ${from.path} β†’ ${to.path}`)
  console.log(`[Time] ${new Date().toISOString()}`)
})

Menggunakan Middleware di Halaman

pages/dashboard.vue
<template>
  <div class="dashboard">
    <h1>Dashboard</h1>
    <p>Selamat datang, {{ user.name }}!</p>
  </div>
</template>

<script setup>
// Define middleware untuk halaman ini
definePageMeta({
  middleware: ['auth'],
  // Gunakan layout tertentu
  layout: 'dashboard'
})

const { user } = useAuth()
</script>

Layouts

layouts/dashboard.vue
<template>
  <div class="dashboard-layout">
    <aside class="sidebar">
      <div class="logo">
        <NuxtLink to="/dashboard">Dashboard</NuxtLink>
      </div>
      <nav>
        <NuxtLink to="/dashboard">Overview</NuxtLink>
        <NuxtLink to="/dashboard/products">Produk</NuxtLink>
        <NuxtLink to="/dashboard/orders">Pesanan</NuxtLink>
        <NuxtLink to="/dashboard/settings">Settings</NuxtLink>
      </nav>
    </aside>

    <main class="main-content">
      <header class="top-bar">
        <h2>{{ pageTitle }}</h2>
        <button @click="logout">Logout</button>
      </header>

      <!-- Slot untuk konten halaman -->
      <slot />
    </main>
  </div>
</template>

<script setup>
const { logout } = useAuth()
const route = useRoute()
const pageTitle = computed(() => route.meta.title || 'Dashboard')
</script>

<style scoped>
.dashboard-layout {
  display: grid;
  grid-template-columns: 250px 1fr;
  min-height: 100vh;
}

.sidebar {
  background: var(--sidebar-bg);
  padding: 1.5rem;
  border-right: 1px solid var(--border-color);
}

.sidebar nav {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  margin-top: 2rem;
}

.main-content {
  padding: 2rem;
}
</style>
layouts/default.vue
<template>
  <div class="default-layout">
    <AppHeader />

    <main class="main-content">
      <slot />
    </main>

    <AppFooter />
  </div>
</template>

<style scoped>
.main-content {
  min-height: calc(100vh - 160px);
  padding: 2rem 1rem;
  max-width: 1200px;
  margin: 0 auto;
}
</style>
πŸ’‘ Tips

Layout default (layouts/default.vue) digunakan secara otomatis jika halaman tidak mendefinisikan layout. Untuk halaman tanpa layout (seperti halaman login), gunakan layout: false di definePageMeta().

11. Deployment

Nuxt mendukung deployment ke berbagai platform berkat Nitro server engine. Berikut beberapa opsi populer:

Deploy ke Vercel

Bash
# Install Vercel CLI
npm i -g vercel

# Deploy langsung dari terminal
vercel

# Atau connect repository GitHub ke Vercel Dashboard
# Vercel otomatis mendeteksi Nuxt dan konfigurasi build

# Environment Variables
# Set di Vercel Dashboard β†’ Settings β†’ Environment Variables
# NUXT_PUBLIC_API_URL=https://api.example.com
# NUXT_SECRET_KEY=your-secret-key

Deploy ke Netlify

netlify.toml
# netlify.toml
[build]
  command = "npm run build"
  publish = ".output/public"

[build.environment]
  NODE_VERSION = "20"

# Redirect SPA fallback
[[redirects]]
  from = "/*"
  to = "/index.html"
  status = 200

Deploy dengan Node.js Server

Bash β€” Production Server
# Build untuk production
npm run build

# Hasil build ada di .output/
# .output/
# β”œβ”€β”€ server/        ← Node.js server
# └── public/        ← Static assets

# Jalankan production server
node .output/server/index.m4

# Atau dengan PM2 (process manager)
pm2 start .output/server/index.m4 --name nuxt-app

# Environment variables
NUXT_PORT=3000
NUXT_HOST=0.0.0.0
NUXT_PUBLIC_API_URL=https://api.example.com

Deploy Static (SSG)

Bash β€” Static Generation
# Generate static files
npx nuxi generate

# Output ada di .output/public/
# Upload folder ini ke CDN manapun:
# - Cloudflare Pages
# - GitHub Pages
# - AWS S3 + CloudFront
# - Azure Static Web Apps

# Preview hasil generate secara lokal
npx nuxi preview
Diagram: Deployment Options
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚              NUXT DEPLOYMENT OPTIONS                 β”‚
β”‚                                                     β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚  β”‚  SSR / Node   β”‚    β”‚      Static (SSG)        β”‚  β”‚
β”‚  β”‚  Server       β”‚    β”‚                          β”‚  β”‚
β”‚  β”‚               β”‚    β”‚  .output/public/         β”‚  β”‚
β”‚  β”‚  .output/     β”‚    β”‚  β”œβ”€β”€ index.html          β”‚  β”‚
β”‚  β”‚  └── server/  β”‚    β”‚  β”œβ”€β”€ about.html          β”‚  β”‚
β”‚  β”‚      └── indexβ”‚    β”‚  └── blog/               β”‚  β”‚
β”‚  β”‚               β”‚    β”‚                          β”‚  β”‚
β”‚  β”‚  Targets:     β”‚    β”‚  Targets:                β”‚  β”‚
β”‚  β”‚  β€’ Vercel     β”‚    β”‚  β€’ Cloudflare Pages      β”‚  β”‚
β”‚  β”‚  β€’ Railway    β”‚    β”‚  β€’ GitHub Pages          β”‚  β”‚
β”‚  β”‚  β€’ DigitalOc. β”‚    β”‚  β€’ Netlify               β”‚  β”‚
β”‚  β”‚  β€’ AWS EC2    β”‚    β”‚  β€’ AWS S3                β”‚  β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

12. Quiz Pemahaman

Uji pemahaman Anda tentang Nuxt.js dengan quiz interaktif berikut!

1. Apa yang dimaksud dengan file-based routing di Nuxt?

2. Composable apa yang digunakan untuk data fetching SSR-safe di Nuxt?

3. Apa peran Nitro dalam Nuxt.js?

4. Bagaimana cara membuat dynamic route /blog/:slug di Nuxt?

5. Fitur Nuxt apa yang memungkinkan Anda tidak perlu menulis import statement untuk komponen?

πŸ“ Ringkasan

Dalam tutorial ini, Anda telah mempelajari:

  • Pengenalan Nuxt.js dan keunggulannya dibanding framework lain
  • Instalasi, setup, dan struktur proyek Nuxt
  • File-based routing dengan dynamic dan nested routes
  • Server-Side Rendering (SSR) dan cara kerjanya
  • Static Site Generation (SSG) dan hybrid rendering
  • Auto-imports untuk komponen, composables, dan utils
  • Membuat dan menggunakan composables
  • Server API routes dengan Nitro
  • Nuxt Modules dan custom modules
  • Middleware dan layouts
  • Deployment ke berbagai platform