Kembali ke Beranda Blog
Web Development 5 min read 18 pembaca

Full-Stack Web Development: Nuxt 3 & Vue 3 Masterclass Guide

A hands-on, multi-chapter engineering tutorial building a full-stack SaaS platform using Nuxt 3 and Vue 3. Features modern Composition API, SSR rendering, Pinia state management, and secure token authentication.

Dahlan

Dahlan

Software Architect • 30 Agustus 2026

Full-Stack Web Development: Nuxt 3 & Vue 3 Masterclass Guide

Chapter 1: Full-Stack Web Development with Nuxt 3 & Vue 3 Architecture

Nuxt 3 fuses the reactivity of Vue 3 (Composition API) with intelligent backend capabilities through the Nitro engine. Utilizing universal rendering architecture, applications can be served via Server-Side Rendering (SSR) for optimal SEO performance or Static Site Generation (SSG) for rapid CDN distribution.

Key Pillars of the Nuxt 3 Ecosystem

Auto-Imports: Core APIs, composables, and Vue components are automatically imported, eliminating manual boilerplate code.

Nitro Server Engine: A versatile backend engine capable of running on Node.js, Serverless Edge, Workers, or modern cloud platforms.

Type-Safe Development: Built-in TypeScript support to catch errors early at compile time.

Pinia Integration: Centralized, modular state management replacing legacy Vuex with high performance.

Chapter 2: Setting Up the Development Environment & Project Initialization

Before starting, ensure your workstation is running Node.js LTS (v18+) or higher.

Run the following command in your terminal to initialize a fresh Nuxt 3 project named NuxtMetrics:

Bash

npx nuxi@latest init nuxt-metrics
cd nuxt-metrics

Install the core dependencies including Tailwind CSS and Pinia for styling and state management:

Bash

npm install @pinia/nuxt pinia
npm install -D @nuxtjs/tailwindcss

Open the primary Nuxt configuration file in the project root and register the installed modules:

File: nuxt.config.ts

TypeScript

// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
  compatibilityDate: '2026-08-27',
  devtools: { enabled: true },
  modules: [
    '@pinia/nuxt',
    '@nuxtjs/tailwindcss'
  ],
  typescript: {
    strict: true,
    typeCheck: true
  },
  app: {
    head: {
      title: 'NuxtMetrics - Enterprise Analytics Platform',
      meta: [
        { name: 'description', content: 'Full-stack application built with Nuxt 3, Vue 3, and Pinia.' }
      ]
    }
  }
})

Chapter 3: Designing Data Models & Type Interfaces

Strict TypeScript usage ensures consistent data contracts from the user interface down to the server routes.

💡 Create a types directory and add the following interface definitions:

File: types/analytics.ts

TypeScript

export interface MetricItem {
  id: string
  title: string
  value: number
  changePercentage: number
  category: 'revenue' | 'traffic' | 'conversion'
  status: 'active' | 'archived'
  updatedAt: string
}

export interface UserProfile {
  id: string
  name: string
  email: string
  role: 'admin' | 'editor' | 'viewer'
}

Chapter 4: Modern State Management Using Pinia

Pinia replaces Vuex as the official state management standard for Vue 3. It offers lightweight memory consumption, full type inference, and seamless integration with the Composition API (defineStore).

💡 Create a store file to manage application analytics data:

File: stores/useMetricsStore.ts

TypeScript

import { defineStore } from 'pinia'
import type { MetricItem } from '~/types/analytics'

export const useMetricsStore = defineStore('metrics', () => {
  const metrics = ref<MetricItem[]>([])
  const isLoading = ref<boolean>(false)
  const errorMessage = ref<string | null>(null)

  const activeMetrics = computed(() => 
    metrics.value.filter((item) => item.status === 'active')
  )

  const totalRevenue = computed(() => 
    metrics.value
      .filter((m) => m.category === 'revenue')
      .reduce((acc, curr) => acc + curr.value, 0)
  )

  async function fetchMetrics(): Promise<void> {
    isLoading.value = true
    errorMessage.value = null
    try {
      const data = await $fetch<MetricItem[]>('/api/v1/metrics')
      metrics.value = data
    } catch (error: any) {
      errorMessage.value = error.message || 'Failed to fetch metrics data.'
    } finally {
      isLoading.value = false
    }
  }

  function addMetric(newMetric: MetricItem): void {
    metrics.value.unshift(newMetric)
  }

  return {
    metrics,
    isLoading,
    errorMessage,
    activeMetrics,
    totalRevenue,
    fetchMetrics,
    addMetric
  }
})

Chapter 5: Reusable Composables for Business Logic Extraction

In Nuxt 3, Composables leverage the Vue Composition API to encapsulate stateful logic, making it reusable across components without code duplication.

💡 Create a composable for currency and date formatting:

File: composables/useFormatters.ts

TypeScript

export const useFormatters = () => {
  const formatCurrency = (amount: number, currency: string = 'USD'): string => {
    return new Intl.NumberFormat('en-US', {
      style: 'currency',
      currency: currency,
      maximumFractionDigits: 0
    }).format(amount)
  }

  const formatDate = (isoString: string): string => {
    return new Date(isoString).toLocaleDateString('en-US', {
      day: 'numeric',
      month: 'short',
      year: 'numeric'
    })
  }

  return {
    formatCurrency,
    formatDate
  }
}

Chapter 6: Backend Endpoints with Nitro Server Routes

Nitro enables full-stack API creation directly inside the server/api/ folder, allowing backend logic to reside within the same codebase.

💡 Create a mock GET endpoint to serve initial analytics data:

File: server/api/v1/metrics.get.ts

TypeScript

import type { MetricItem } from '~/types/analytics'

export default defineEventHandler(async (event): Promise<MetricItem[]> => {
  // Simulate database query / asynchronous delay
  await new Promise((resolve) => setTimeout(resolve, 300))

  return [
    {
      id: 'm-1',
      title: 'Enterprise Monthly Recurring Revenue',
      value: 48500,
      changePercentage: 12.5,
      category: 'revenue',
      status: 'active',
      updatedAt: new Date().toISOString()
    },
    {
      id: 'm-2',
      title: 'Active Platform Users',
      value: 12400,
      changePercentage: 8.2,
      category: 'traffic',
      status: 'active',
      updatedAt: new Date().toISOString()
    },
    {
      id: 'm-3',
      title: 'Checkout Conversion Rate',
      value: 3.4,
      changePercentage: -0.4,
      category: 'conversion',
      status: 'active',
      updatedAt: new Date().toISOString()
    }
  ]
})

💡 Create a POST endpoint to handle new metric creation:

File: server/api/v1/metrics.post.ts

TypeScript

import type { MetricItem } from '~/types/analytics'

export default defineEventHandler(async (event): Promise<MetricItem> => {
  const body = await readBody(event)

  if (!body.title || !body.value) {
    throw createError({
      statusCode: 400,
      statusMessage: 'Title and Value fields are required.'
    })
  }

  const newMetric: MetricItem = {
    id: `m-${Date.now()}`,
    title: body.title,
    value: Number(body.value),
    changePercentage: Number(body.changePercentage || 0),
    category: body.category || 'revenue',
    status: 'active',
    updatedAt: new Date().toISOString()
  }

  return newMetric
})

Chapter 7: App Layout & Global Navigation Structure

Layout components wrap page content to ensure consistent headers, sidebars, and footers across the web application.

💡 Create the primary layout template:

File: layouts/default.vue

Code snippet

<template>
  <div class="min-h-screen bg-slate-50 text-slate-900 font-sans antialiased">
    <!-- Header Navigation -->
    <header class="bg-indigo-600 text-white shadow-md">
      <div class="max-w-7xl mx-auto px-6 py-4 flex justify-between items-center">
        <NuxtLink to="/" class="text-xl font-bold tracking-tight hover:opacity-90 transition">
          NuxtMetrics <span class="text-xs bg-indigo-800 px-2 py-0.5 rounded-full font-normal">v3.x</span>
        </NuxtLink>
        <nav class="flex space-x-6 text-sm font-medium">
          <NuxtLink to="/" class="hover:underline" active-class="font-bold underline">Dashboard</NuxtLink>
          <NuxtLink to="/analytics" class="hover:underline" active-class="font-bold underline">Analytics</NuxtLink>
        </nav>
      </div>
    </header>

    <!-- Main Dynamic Content Slot -->
    <main class="max-w-7xl mx-auto px-6 py-8">
      <slot />
    </main>
  </div>
</template>

Chapter 8: Building Reusable UI Components

Vue 3 components are structured cleanly using <script setup lang="ts">. The component below displays individual metrics visually.

File: components/MetricCard.vue

Code snippet

<script setup lang="ts">
import type { MetricItem } from '~/types/analytics'

const props = defineProps<{
  item: MetricItem
}>()

const { formatCurrency } = useFormatters()

const isPositive = computed(() => props.item.changePercentage >= 0)
</script>

<template>
  <div class="bg-white p-6 rounded-xl border border-slate-200 shadow-sm hover:shadow-md transition">
    <div class="flex justify-between items-start mb-2">
      <span class="text-xs font-semibold uppercase tracking-wider text-slate-400">
        {{ item.category }}
      </span>
      <span 
        :class="[
          'text-xs font-bold px-2 py-0.5 rounded-full',
          isPositive ? 'bg-emerald-100 text-emerald-700' : 'bg-rose-100 text-rose-700'
        ]"
      >
        {{ isPositive ? '+' : '' }}{{ item.changePercentage }}%
      </span>
    </div>

    <h3 class="text-sm font-medium text-slate-600 mb-1">{{ item.title }}</h3>
    
    <p class="text-2xl font-extrabold text-slate-900">
      {{ item.category === 'revenue' ? formatCurrency(item.value) : item.value.toLocaleString() }}
    </p>

    <p class="text-xs text-slate-400 mt-4">
      Updated: {{ useFormatters().formatDate(item.updatedAt) }}
    </p>
  </div>
</template>

Chapter 9: Constructing the Dashboard Page (SSR & Client Form Submission)

Nuxt 3 uses file-based routing inside the pages/ directory. The page below uses Server-Side Rendering (SSR) for initial data fetching and handles reactive form submissions on the client side.

File: pages/index.vue

Code snippet

<script setup lang="ts">
import { useMetricsStore } from '~/stores/useMetricsStore'

const metricsStore = useMetricsStore()

// SSR Data Fetching on server render / route navigation
await useAsyncData('init-metrics', () => metricsStore.fetchMetrics())

// Local Form State
const form = reactive({
  title: '',
  value: 0,
  category: 'revenue' as 'revenue' | 'traffic' | 'conversion',
  changePercentage: 0
})

const isSubmitting = ref(false)

async function handleCreateMetric() {
  if (!form.title || form.value <= 0) return

  isSubmitting.value = true
  try {
    const createdItem = await $fetch('/api/v1/metrics', {
      method: 'POST',
      body: form
    })
    
    metricsStore.addMetric(createdItem)

    // Reset Form
    form.title = ''
    form.value = 0
    form.changePercentage = 0
  } catch (error: any) {
    alert(error.statusMessage || 'Failed to save metric data.')
  } finally {
    isSubmitting.value = false
  }
}
</script>

<template>
  <div>
    <div class="mb-8">
      <h1 class="text-2xl font-extrabold text-slate-900">Real-Time Performance Dashboard</h1>
      <p class="text-sm text-slate-500">Monitor key business metrics seamlessly.</p>
    </div>

    <!-- Total Revenue Summary Banner -->
    <div class="bg-indigo-900 text-white p-6 rounded-xl mb-8 shadow-sm flex justify-between items-center">
      <div>
        <p class="text-xs text-indigo-200 uppercase tracking-wider font-semibold">Total Accumulated Revenue</p>
        <p class="text-3xl font-black mt-1">{{ useFormatters().formatCurrency(metricsStore.totalRevenue) }}</p>
      </div>
      <span class="text-xs bg-indigo-700 px-3 py-1 rounded-lg border border-indigo-500/50">Live Sync</span>
    </div>

    <div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
      <!-- Metric Cards Display Grid -->
      <div class="lg:col-span-2 grid grid-cols-1 md:grid-cols-2 gap-4">
        <MetricCard 
          v-for="item in metricsStore.metrics" 
          :key="item.id" 
          :item="item" 
        />
      </div>

      <!-- New Metric Input Form -->
      <div class="bg-white p-6 rounded-xl border border-slate-200 shadow-sm h-fit">
        <h2 class="text-base font-bold text-slate-800 mb-4">Add New Metric</h2>
        
        <form @submit.prevent="handleCreateMetric" class="space-y-4">
          <div>
            <label class="block text-xs font-semibold text-slate-600 mb-1">Metric Title</label>
            <input 
              v-model="form.title" 
              type="text" 
              required
              class="w-full text-sm border border-slate-300 rounded-lg p-2.5 focus:ring-2 focus:ring-indigo-500 outline-none" 
              placeholder="e.g. Q3 Sales Goal"
            />
          </div>

          <div>
            <label class="block text-xs font-semibold text-slate-600 mb-1">Category</label>
            <select 
              v-model="form.category"
              class="w-full text-sm border border-slate-300 rounded-lg p-2.5 focus:ring-2 focus:ring-indigo-500 outline-none"
            >
              <option value="revenue">Revenue</option>
              <option value="traffic">Traffic</option>
              <option value="conversion">Conversion</option>
            </select>
          </div>

          <div>
            <label class="block text-xs font-semibold text-slate-600 mb-1">Value</label>
            <input 
              v-model.number="form.value" 
              type="number" 
              required
              class="w-full text-sm border border-slate-300 rounded-lg p-2.5 focus:ring-2 focus:ring-indigo-500 outline-none" 
            />
          </div>

          <div>
            <label class="block text-xs font-semibold text-slate-600 mb-1">Change (%)</label>
            <input 
              v-model.number="form.changePercentage" 
              type="number" 
              step="0.1"
              class="w-full text-sm border border-slate-300 rounded-lg p-2.5 focus:ring-2 focus:ring-indigo-500 outline-none" 
            />
          </div>

          <button 
            type="submit" 
            :disabled="isSubmitting"
            class="w-full bg-indigo-600 hover:bg-indigo-700 text-white font-semibold text-sm py-2.5 rounded-lg transition disabled:opacity-50"
          >
            {{ isSubmitting ? 'Saving...' : 'Save Metric' }}
          </button>
        </form>
      </div>
    </div>
  </div>
</template>

Chapter 10: Authentication Handling with HttpOnly Cookies & Middleware

Securing authentication via HttpOnly Cookies protects user tokens from malicious client-side access (XSS attacks).

💡 Create a route middleware to restrict unauthorized access:

File: middleware/auth.ts

TypeScript

export default defineNuxtRouteMiddleware((to, from) => {
  const authToken = useCookie('auth_token')

  // Redirect to login if authentication cookie is missing
  if (!authToken.value) {
    return navigateTo('/login')
  }
})

Apply the middleware to a protected route:

File: pages/analytics.vue

Code snippet

<script setup lang="ts">
definePageMeta({
  middleware: 'auth'
})
</script>

<template>
  <div class="bg-white p-8 rounded-xl border border-slate-200">
    <h1 class="text-xl font-bold text-slate-800">Advanced Analytics Dashboard</h1>
    <p class="text-sm text-slate-500 mt-2">This protected route is accessible only after cookie authentication validation.</p>
  </div>
</template>

Chapter 11: Unit & Integration Testing with Vitest

To maintain code reliability, build test cases using Vitest and @vue/test-utils.

Install testing tools in your project:

Bash

npm install -D vitest @vue/test-utils @nuxt/test-utils

Create a unit test file for composables:

File: tests/composables/useFormatters.spec.ts

TypeScript

import { describe, it, expect } from 'vitest'
import { useFormatters } from '~/composables/useFormatters'

describe('useFormatters Composable', () => {
  const { formatCurrency } = useFormatters()

  it('formats numeric values into valid USD currency strings', () => {
    const formatted = formatCurrency(5000)
    expect(formatted).toContain('$5,000')
  })

  it('handles zero values correctly', () => {
    const formatted = formatCurrency(0)
    expect(formatted).toContain('$0')
  })
})

Run tests from the command line:

Bash

npx vitest run

Chapter 12: Production Build, Deployment & SEO Optimization

Before deploying your Nuxt 3 application to production servers or cloud providers, compile assets for optimal client performance.

Run the production build command:

Bash

# Build the application for Node.js server deployment
npm run build

The output build will reside in .output/. Run it directly using Node.js:

Bash

node .output/server/index.mjs

Production Checklist for Nuxt 3

  1. Pre-rendering Static Routes: Configure routeRules in nuxt.config.ts to enable Incremental Static Regeneration (ISR) for pages with infrequent data updates.

  2. Compress Assets: Ensure web servers (such as NGINX or Cloudflare) have Brotli/Gzip compression enabled for static assets.

  3. Environment Variables: Store confidential keys in .env files and access them using useRuntimeConfig().

Chapter 13: Comparative Matrix Table: Modern Frontend Frameworks

Feature / Architecture

Nuxt 3 (Vue 3 Engine)

Next.js 15 (React Engine)

Vanilla Vue 3 SPA

Rendering Model

Universal (SSR, SSG, CSR, ISR).

Server Components & SSR.

Pure Client-Side Rendering (CSR).

Server Engine

Nitro Engine (Multi-platform target).

Node.js Server Runtime.

None (Requires external API backend).

Routing & Auto-Imports

Automatic file-based routing & auto-imports.

Automatic App Router file system.

Manual configuration (vue-router).

State Management

Native Pinia Integration.

Zustand / Redux / Context API.

Pinia / Vuex.

SEO Capabilities

High (Complete HTML rendered from server).

High (Complete HTML rendered from server).

Low (Dependent on browser JavaScript).

Chapter 14: Frequently Asked Questions (FAQ)

What is the primary difference between Vue 3 and Nuxt 3?

Vue 3 is a UI component library for building interactive interfaces. Nuxt 3 is a full-stack framework built on top of Vue 3 that adds Server-Side Rendering (SSR), automatic route generation, SEO tools, and server API handlers in a single ecosystem.

Can Nuxt 3 be used without a standalone backend framework?

Yes! Thanks to the embedded Nitro server engine, you can write custom REST API handlers, server middleware, and database integrations directly inside the server/api/ folder without needing an external Express or Laravel backend.

How does Nuxt 3 manage dynamic SEO metadata across pages?

You can use the built-in useHead() or useSeoMeta() composables inside any Vue component to dynamically set meta titles, descriptions, OpenGraph tags, and canonical links based on server data.

Komentar Pengunjung (0)

Belum ada komentar. Jadilah yang pertama memberikan komentar!