Nuxt.js Guide — Vue.js Framework for Server-Side Rendering
Nuxt.js is a powerful Vue.js framework that simplifies building server-side rendered (SSR), statically generated (SSG), and client-side rendered Vue applications with conventions like file-based routing, auto-imports, and a modular plugin ecosystem.
What You’ll Learn
You’ll understand Nuxt’s directory conventions, configure SSR vs SSG, navigate file-based routing, fetch data with useFetch and useAsyncData, apply middleware, extend with modules, and deploy Nuxt apps to production.
Why Nuxt.js Matters
Nuxt.js eliminates the hardest parts of configuring Vue for production: routing, SSR hydration, build tooling, and data fetching. It’s the recommended framework for any serious Vue project that needs SEO, performance, or scalability. Doda Browser’s documentation portal uses Nuxt 3 with static generation for fast page loads and excellent search engine rankings.
Nuxt.js Learning Path
flowchart LR
A[Vue.js Basics] --> B[Nuxt.js]
B --> C[File-Based Routing]
C --> D[Data Fetching]
D --> E[Middleware & Plugins]
E --> F[Modules & Build Config]
F --> G[Deploy]
B:::current
classDef current fill:#00C58E,color:#fff,stroke:#333,stroke-width:2px
How Nuxt.js Differs from Standard Vue
Standard Vue apps need manual setup for routing, SSR, and build tooling. Nuxt provides all of this out of the box:
- File-based routing — Every
.vuefile inpages/becomes a route automatically - Auto-imports — Composables, components, and utilities are imported automatically
- Hybrid rendering — SSR, SSG, or CSR per route, configurable in
nuxt.config.ts - Server routes — API endpoints live in
server/alongside your frontend
Project Structure
A Nuxt 3 project follows a convention-based structure:
my-nuxt-app/
├── app.vue # Root component
├── pages/ # File-based routes
│ ├── index.vue
│ ├── about.vue
│ └── blog/
│ ├── index.vue
│ └── [slug].vue
├── components/ # Auto-imported components
├── composables/ # Auto-imported composables
├── layouts/ # Page layouts
├── middleware/ # Route middleware
├── server/ # Server routes & API
├── public/ # Static assets
└── nuxt.config.ts # ConfigurationEach folder has a specific purpose. The pages/ directory is the most important — its file structure maps directly to URL paths.
File-Based Routing
Nuxt’s file-based routing is intuitive and eliminates manual route configuration.
Basic Routes
| File | URL Path |
|---|---|
pages/index.vue | / |
pages/about.vue | /about |
pages/contact.vue | /contact |
Dynamic Routes
Use square brackets for URL parameters:
<!-- pages/blog/[slug].vue -->
<template>
<article>
<h1>{{ post.title }}</h1>
<div>{{ post.content }}</div>
</article>
</template>
<script setup>
const route = useRoute();
const slug = route.params.slug;
// Fetch post by slug
const { data: post } = await useFetch(`/api/posts/${slug}`);
</script>Output: Visiting /blog/hello-world renders the component with slug set to "hello-world" and fetches the matching blog post from the API.
Nested Routes
Directories create nested routes automatically:
<!-- pages/dashboard/settings.vue -->
<template>
<div>
<h1>Dashboard Settings</h1>
<p>Configure your preferences here.</p>
</div>
</template>Output: Accessible at /dashboard/settings with no route configuration needed.
Data Fetching
Nuxt 3 provides two composables for data fetching on the server and client.
useFetch
The simplest way to fetch data — works in both SSR and client-side contexts:
<script setup>
const { data: users, pending, error, refresh } = useFetch('/api/users', {
method: 'GET',
headers: { 'Content-Type': 'application/json' }
});
// useFetch automatically handles:
// - Server-side fetching during SSR
// - Client-side hydration without re-fetching
// - Loading and error states
// - Deduplication of identical requests
</script>
<template>
<div>
<p v-if="pending">Loading users...</p>
<p v-else-if="error">Failed to load: {{ error.message }}</p>
<ul v-else>
<li v-for="user in users" :key="user.id">{{ user.name }}</li>
</ul>
</div>
</template>Output: During SSR, users are fetched on the server and HTML is rendered with the data. On the client, the component hydrates without re-fetching. Subsequent navigations fetch client-side.
useAsyncData
More control than useFetch, suitable for custom fetching logic:
<script setup>
const { data: stats } = useAsyncData('dashboard-stats', async () => {
// Custom async logic
const response = await fetch('https://api.example.com/stats');
const json = await response.json();
// Transform data before returning
return {
totalUsers: json.users.length,
activeToday: json.users.filter(u => u.lastSeen > Date.now() - 86400000).length,
averageSession: json.users.reduce((sum, u) => sum + u.sessionDuration, 0) / json.users.length
};
});
// useAsyncData accepts a unique key ('dashboard-stats') to prevent
// duplicate fetches across components
</script>
<template>
<div class="stats-grid">
<div class="stat-card">
<h3>Total Users</h3>
<p class="stat-value">{{ stats.totalUsers }}</p>
</div>
<div class="stat-card">
<h3>Active Today</h3>
<p class="stat-value">{{ stats.activeToday }}</p>
</div>
<div class="stat-card">
<h3>Avg Session</h3>
<p class="stat-value">{{ Math.round(stats.averageSession) }}s</p>
</div>
</div>
</template>Output: The stats are fetched once on the server during SSR and hydrated on the client. The unique key ensures that multiple components using the same data don’t cause duplicate network requests.
Middleware
Middleware runs before a route is rendered. Use it for authentication, redirects, or data preloading.
// middleware/auth.ts
export default defineNuxtRouteMiddleware((to, from) => {
const token = useCookie('auth-token');
if (!token.value && to.path !== '/login') {
// Abort navigation and redirect to login
return navigateTo('/login');
}
// If no return or undefined, navigation proceeds
});Apply middleware globally (add _ prefix) or per-page:
<!-- pages/dashboard.vue -->
<script setup>
definePageMeta({
middleware: 'auth'
});
</script>
<template>
<div>
<h1>Protected Dashboard</h1>
<p>Only authenticated users see this.</p>
</div>
</template>Output: Visiting /dashboard without a valid auth cookie redirects to /login. With a valid cookie, the dashboard renders normally.
Modules
Nuxt’s modular architecture lets you extend functionality with npm packages:
// nuxt.config.ts
export default defineNuxtConfig({
modules: [
'@nuxtjs/tailwindcss', // Tailwind CSS
'@nuxtjs/color-mode', // Dark/light mode
'@nuxt/image', // Optimized images
'@pinia/nuxt', // State management
],
// Module-specific configuration
colorMode: {
classSuffix: '',
preference: 'system'
},
image: {
format: ['webp'],
screens: {
xs: 320,
sm: 640,
md: 768,
lg: 1024,
xl: 1280
}
}
});Output: Modules are loaded automatically and add their features (CSS utilities, image optimization, state management) to the app without manual setup.
Deployment
Nuxt apps deploy as SSR, SSG, or hybrid. Choose based on your needs:
# Static generation (Jamstack) — best for blogs, docs
npx nuxi generate
# Output: .output/public/ — deploy to any static host
# SSR with Node.js server
npx nuxi build
node .output/server/index.mjs
# Deploy to VPS, Cloud Run, or Railway
# Serverless deployment (Vercel, Netlify)
npx nuxi build --preset vercel
# Or set preset in nuxt.config.ts// nuxt.config.ts — Hybrid rendering
export default defineNuxtConfig({
routeRules: {
'/': { prerender: true }, // Static
'/blog/**': { ssr: true }, // SSR
'/dashboard/**': { ssr: false }, // SPA
'/api/**': { cors: true, redirect: '/api' } // API
}
});Output: Each route follows its configured rendering strategy. The marketing pages are statically pre-rendered, blog posts use SSR for SEO, and the dashboard is a client-side SPA.
Security Angle: Input Validation in API Routes
// server/api/users.post.ts
export default defineEventHandler(async (event) => {
const body = await readBody(event);
// Validate input before processing
if (!body.email || !body.email.includes('@')) {
throw createError({
statusCode: 400,
statusMessage: 'Invalid email address'
});
}
// Sanitize string inputs
const sanitizedName = body.name.replace(/[<>]/g, '');
// Rate limiting check (simplified)
const ip = getHeader(event, 'x-forwarded-for') || event.node.req.socket.remoteAddress;
// Process validated data
return { success: true, user: await createUser(sanitizedName, body.email) };
});DodaTech uses this same validation pattern in Durga Antivirus Pro’s API endpoints to prevent injection attacks when processing user-submitted file metadata.
Common Mistakes Beginners Make
Forgetting
awaitin data fetching: BothuseFetchanduseAsyncDatareturn promises. Withoutawait, components render before data arrives, causingundefinederrors.Mutating fetched data directly: Never modify
data.valuedirectly. Create a reactive copy withreforreactivebefore mutating.Using
<script>instead of<script setup>: Nuxt 3’s Composition API works best with<script setup>. The Options API (export default) lacks auto-import benefits.Not closing the
pages/directory: Any.vuefile inpages/becomes a route. Accidentally leaving draft files there creates unintended URLs.Mixing SSR and browser APIs:
localStorage,window, anddocumentaren’t available during SSR. Wrap browser-only code inonMounted()or useclient-onlycomponents.Missing
definePageMetafor layout switching: Each page defaults tolayouts/default.vue. For different layouts, you must explicitly setdefinePageMeta({ layout: 'custom' }).Over-fetching in static generation:
useFetchwithserver: true(default) fetches during SSG build. Ensure API endpoints are reachable at build time or setserver: false.
Practice Questions
- What is the difference between
useFetchanduseAsyncData? - How do you create a dynamic route for blog posts with slugs like
/blog/my-post? - What does middleware return to redirect a user?
- How do you configure a page to render as a client-side SPA instead of SSR?
- What happens if you use
windowin a<script setup>block during SSR?
Answers:
useFetchis a wrapper arounduseAsyncDatathat auto-generates keys and handles fetching with options likebaseURLandheaders.useAsyncDatagives more control over the fetch implementation and requires a manual key.- Create
pages/blog/[slug].vue. Theslugparameter is accessed viauseRoute().params.slug. - Return
navigateTo('/path')to redirect, orabortNavigation()to cancel navigation entirely. - Add
definePageMeta({ ssr: false })in the page’s<script setup>block. - Nuxt throws an error during SSR because
windowdoesn’t exist on the server. UseonMounted(() => { ... })to access browser APIs.
Challenge
Build a full-featured blog with Nuxt 3: file-based routes for posts using dynamic slugs, a tags system with filtered views, pagination with useFetch, a search page that queries the API, and middleware that redirects draft posts to a 404 page.
Real-World Task
Create a documentation site (like DodaTech’s) using Nuxt 3 with static generation. Use @nuxt/content for Markdown-based pages, add a sidebar navigation component, implement search with useFetch against a local index, and deploy to Cloudflare Pages or Netlify.
FAQ
Try It Yourself
# Create a new Nuxt 3 project
npx nuxi@latest init my-nuxt-app
cd my-nuxt-app
npm install
npm run devCreate three pages: index.vue (homepage with hero), about.vue (team section), and blog/[slug].vue (dynamic blog post). Add navigation between them using <NuxtLink>, fetch data on the blog page using useFetch, and apply a custom layout with header and footer.
What’s Next
Related topics: Vue, JavaScript, TypeScript, Node.js
What’s Next
Congratulations on completing this Nuxt.js tutorial! Here’s where to go from here:
- Practice daily — Consistency is more important than long study sessions
- Build a project — Apply what you learned by building something real
- Explore related topics — Check out other tutorials in the same category
- Join the community — Discuss with other learners and share your progress
Remember: every expert was once a beginner. Keep coding!
Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro