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 Configuration | Routing, bundling, dan optimasi bekerja out-of-the-box tanpa setup manual |
| SSR & SSG | Dukungan server-side rendering dan static generation dalam satu framework |
| Auto-Imports | Komponen, composables, dan utils di-import otomatis tanpa deklarasi manual |
| Server Engine (Nitro) | Server API, middleware server, dan deploy ke berbagai platform cloud |
| SEO-Friendly | Meta tags, sitemap, dan prerendering built-in untuk SEO optimal |
| Ekosistem Modules | Ratusan modul siap pakai untuk auth, CMS, analytics, UI, dan lainnya |
Nuxt.js vs Alternatif Lain
| Aspek | Nuxt.js | Next.js | Astro |
|---|---|---|---|
| Base Framework | Vue.js | React | Multi (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 |
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
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
// 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
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.
<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 — 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
<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 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' }
}
})
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?
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
<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>
<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
<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
// 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
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
// 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
// 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
// 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
// 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
// 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
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/tailwindcss | Tailwind CSS integration | npm i @nuxtjs/tailwindcss |
| @pinia/nuxt | State management Pinia | npm i @pinia/nuxt |
| @nuxt/image | Auto image optimization | npm i @nuxt/image |
| @nuxt/content | Markdown/CMS content | npm i @nuxt/content |
| @nuxtjs/sitemap | Auto sitemap generation | npm i @nuxtjs/sitemap |
| nuxt-icon | Icon library (Iconify) | npm i nuxt-icon |
| @vueuse/nuxt | VueUse composables | npm i @vueuse/nuxt |
| @nuxtjs/color-mode | Dark/light mode toggle | npm i @nuxtjs/color-mode |
Membuat Custom Module
// 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 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
// 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
<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
<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>
<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>
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
# 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
[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
# 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)
# 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
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?
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