Skip to content

Nuxt.js SSR — Server-Side Rendering with Vue.js and Nuxt

DodaTech Updated 2026-06-28 6 min read

In this tutorial, you will learn about Nuxt.js SSR. We cover key concepts, practical examples, and best practices to help you master this topic.

Nuxt.js SSR provides server-side rendering for Vue.js applications with file-based routing, asyncData for server data fetching, automatic code splitting, auto-imports, and a powerful module ecosystem.

What You'll Learn

By the end of this tutorial, you will understand how to build SSR applications with Nuxt.js, how asyncData and useFetch work for server data fetching, how the file-based routing system works, how to use plugins and modules, and how Nuxt SSR compares to Next.js.

Why It Matters

Nuxt.js is the SSR framework for Vue.js, analogous to Next.js for React. It provides a complete SSR solution with built-in data fetching, automatic code splitting, and deployment options. Understanding Nuxt SSR is essential for Vue.js developers building production-ready applications that need SEO and fast initial loads.

Real-World Use

A job board built with Vue.js migrated to Nuxt.js for SSR. Job listing pages went from blank (client-rendered) to fully indexed by Google within days. Organic traffic increased 180 percent. The Migration involved renaming a few files and replacing client-side fetch calls with asyncData.

Nuxt.js SSR Architecture
    ┌──────────────────────────────────────────────────────────┐
    │              Nuxt.js SSR Flow                            │
    ├──────────────────────────────────────────────────────────┤
    │                                                          │
    │  Request → Nuxt Server                                   │
    │              │                                           │
    │              1. Server receives request                   │
    │              2. Nuxt middleware runs                      │
    │              3. Route matched from pages/                 │
    │              4. asyncData or useFetch runs on server      │
    │              5. Vue component renders to HTML             │
    │              6. HTML sent to browser                     │
    │              7. Client hydrates Vue app                  │
    │              8. App becomes interactive                  │
    │                                                          │
    │  Key Features:                                           │
    │    • File-based routing (pages/)                         │
    │    • Auto-imports (no manual imports needed)             │
    │    • Module system (Auth, SEO, PWA)                     │
    │    • Automatic code splitting                            │
    │    • Static and SSR modes                               │
    │    • Middleware support                                  │
    │                                                          │
    └──────────────────────────────────────────────────────────┘

Think of Nuxt.js like a fully-furnished apartment for Vue.js developers. Vue.js gives you the empty rooms (components). Nuxt adds the furniture (routing, data fetching, SSR), utilities (auto-imports), and building management (build system, deployment). You move in and start living immediately instead of shopping for furniture.

Page Setup with asyncData

<!-- pages/index.vue — Nuxt page with SSR data fetching -->
<template>
  <div>
    <h1>{{ pageTitle }}</h1>
    <div class="product-grid">
      <ProductCard
        v-for="product in products"
        :key="product.id"
        :product="product"
      />
    </div>
  </div>
</template>

<script setup>
// useAsyncData — fetches data on the server (SSR) or client (SPA)
const { data: products, pending, error } = await useAsyncData('products', () =>
  $fetch('https://api.example.com/products')
);

// useFetch — convenience wrapper for useAsyncData + $fetch
const { data: featured } = await useFetch('/api/featured-products', {
  baseURL: 'https://api.example.com',
  // SSR options
  server: true,       // Fetch on server (default)
  lazy: false,        // Wait for data before rendering (default)
  pick: ['id', 'name', 'price'],  // Pick specific fields
});

// Computed property from fetched data
const pageTitle = computed(() =>
  featured.value?.length
    ? `Featured Products (${featured.value.length})`
    : 'Product Catalog'
);
</script>

Dynamic Routes and Middleware

<!-- pages/products/[id].vue — Dynamic product page -->
<template>
  <div v-if="pending">Loading product...</div>
  <div v-else-if="error">Product not found</div>
  <div v-else>
    <h1>{{ product.name }}</h1>
    <p>{{ product.description }}</p>
    <p class="price">${{ product.price }}</p>

    <button @click="addToCart(product.id)">
      Add to Cart
    </button>
  </div>
</template>

<script setup>
const route = useRoute();
const { id } = route.params;

const { data: product, pending, error } = await useFetch(
  `https://api.example.com/products/${id}`
);

// Set page meta for SEO
useHead({
  title: computed(() => product.value?.name
    ? `${product.value.name} — My Store`
    : 'Product'
  ),
  meta: [
    { name: 'description', content: computed(() => product.value?.description) }
  ]
});

function addToCart(productId) {
  // Client-side interaction
}
</script>

<!-- middleware/auth.js — Authentication middleware -->
export default defineNuxtRouteMiddleware((to, from) => {
  const token = useCookie('auth-token');

  if (!token.value && to.path !== '/login') {
    return navigateTo('/login');
  }
});

// Apply middleware in pages:
// pages/dashboard.vue
<script setup>
definePageMeta({
  middleware: 'auth'
  // or ['auth', 'another-middleware']
});
</script>

Layouts and Plugins

<!-- layouts/default.vue — Default layout wrapper -->
<template>
  <div>
    <header>
      <nav>
        <NuxtLink to="/">Home</NuxtLink>
        <NuxtLink to="/products">Products</NuxtLink>
        <NuxtLink to="/about">About</NuxtLink>
      </nav>
    </header>

    <main>
      <slot /> <!-- Page content injected here -->
    </main>

    <footer>
      <p>&copy; 2026 My Store</p>
    </footer>
  </div>
</template>

<!-- plugins/api.client.js — Client-only plugin -->
export default defineNuxtPlugin(() => {
  return {
    provide: {
      analytics: {
        trackPage: (page) => {
          if (process.client) {
            window.gtag('config', 'GA_MEASUREMENT_ID', {
              page_path: page
            });
          }
        }
      }
    }
  };
});

// Usage in component:
// const { $analytics } = useNuxtApp();
// $analytics.trackPage('/products');

Common Mistakes

  1. Using window or document in setup without checks. During SSR, the setup function runs on the server where browser APIs are not available. Check Process.client or use onMounted for browser-only code.
  2. Not handling the pending state. useAsyncData and useFetch return pending state. Always show loading UI for SSR pages, especially when using lazy: true.
  3. Forgetting to set useHead for SEO. Nuxt automatically merges head tags, but you must set title and meta descriptions in each page for proper SEO.
  4. Overusing client-side plugins. Plugins marked with .client.js extension only run in the browser. Use server plugins (.server.js) for server-side logic.
  5. Not using the correct data fetching method. useFetch is for simple API calls. useAsyncData with a custom fetch function gives more control. Choose based on your needs.

Practice Questions

  1. How does Nuxt.js handle SSR data fetching with useAsyncData?
  2. What is the difference between useFetch and useAsyncData?
  3. How do you create dynamic routes in Nuxt.js?
  4. How do you use middleware for authentication in Nuxt?
  5. How do you set page title and meta tags for SEO in Nuxt?

Challenge: Build a job board with Nuxt.js: home page with useFetch for latest jobs listing, dynamic job detail page (pages/jobs/[id].vue), search page with query parameters, authentication middleware for the employer dashboard, SEO meta tags with useHead, and a plugin for analytics tracking.

FAQ

What is the difference between Nuxt 2 and Nuxt 3?

Nuxt 3 uses Vue 3, Vite, Nitro server engine, and Composition API. Nuxt 2 used Vue 2, Webpack, and Options API. Nuxt 3 is the recommended version.

Can I use Nuxt without SSR?

Yes. Nuxt supports multiple rendering modes: SSR (server-side rendering), SSG (static generation), and SPA (client-side only). Configure in nuxt.config.ts.

How does Nuxt handle code splitting?

Nuxt automatically code-splits by pages. Each page loads only its own JavaScript. Components shared across pages are automatically extracted into common chunks.

Does Nuxt support TypeScript?

Yes. Nuxt 3 has first-class TypeScript support. Use .vue files with