# Nuxt 3 ## Project Structure ``` my-nuxt-app/ ├── app.vue # Root component (optional) ├── nuxt.config.ts # Nuxt configuration ├── package.json ├── tsconfig.json ├── .output/ # Build output ├── assets/ # Uncompiled assets (CSS, images) ├── public/ # Static files (served at root) ├── components/ # Auto-imported components │ ├── AppHeader.vue │ └── base/ │ └── Button.vue # Used as ├── composables/ # Auto-imported composables │ └── useAuth.ts ├── layouts/ # Layout components │ ├── default.vue │ └── admin.vue ├── middleware/ # Route middleware │ └── auth.ts ├── pages/ # File-based routing │ ├── index.vue # / │ ├── about.vue # /about │ ├── users/ │ │ ├── index.vue # /users │ │ └── [id].vue # /users/:id │ └── [...slug].vue # Catch-all route ├── plugins/ # Plugins │ └── api.ts ├── server/ # Server API routes │ ├── api/ │ │ └── users.ts # /api/users │ └── middleware/ │ └── log.ts └── stores/ # Pinia stores └── user.ts ``` ## File-based Routing ```vue ``` ## Layouts ```vue ``` ## Data Fetching ```vue ``` ## Server API Routes ```typescript // server/api/users.get.ts export default defineEventHandler(async (event) => { const query = getQuery(event) const page = Number(query.page) || 1 const limit = Number(query.limit) || 10 // Fetch from database const users = await prisma.user.findMany({ skip: (page - 1) * limit, take: limit }) return users }) // server/api/users/[id].get.ts export default defineEventHandler(async (event) => { const id = getRouterParam(event, 'id') const user = await prisma.user.findUnique({ where: { id: Number(id) } }) if (!user) { throw createError({ statusCode: 404, message: 'User not found' }) } return user }) // server/api/users.post.ts export default defineEventHandler(async (event) => { const body = await readBody(event) // Validate if (!body.email || !body.name) { throw createError({ statusCode: 400, message: 'Email and name are required' }) } const user = await prisma.user.create({ data: { email: body.email, name: body.name } }) return user }) // server/api/auth/login.post.ts export default defineEventHandler(async (event) => { const { email, password } = await readBody(event) // Verify credentials const user = await verifyCredentials(email, password) if (!user) { throw createError({ statusCode: 401, message: 'Invalid credentials' }) } // Set session cookie setCookie(event, 'session', user.sessionToken, { httpOnly: true, secure: true, sameSite: 'strict', maxAge: 60 * 60 * 24 * 7 // 7 days }) return { success: true, user } }) ``` ## Middleware ```typescript // middleware/auth.ts - Route middleware export default defineNuxtRouteMiddleware((to, from) => { const { isLoggedIn } = useAuthStore() if (!isLoggedIn) { return navigateTo('/login') } }) // middleware/logger.global.ts - Global middleware export default defineNuxtRouteMiddleware((to, from) => { console.log(`Navigating from ${from.path} to ${to.path}`) }) // server/middleware/log.ts - Server middleware export default defineEventHandler((event) => { console.log(`[${event.method}] ${event.path}`) }) ``` ## Composables ```typescript // composables/useAuth.ts - Auto-imported export const useAuth = () => { const user = useState('user', () => null) const isLoggedIn = computed(() => user.value !== null) async function login(email: string, password: string) { const { data, error } = await useFetch('/api/auth/login', { method: 'POST', body: { email, password } }) if (data.value) { user.value = data.value.user } return { data, error } } async function logout() { await useFetch('/api/auth/logout', { method: 'POST' }) user.value = null navigateTo('/login') } async function fetchUser() { const { data } = await useFetch('/api/auth/me') user.value = data.value } return { user, isLoggedIn, login, logout, fetchUser } } // Usage in component (auto-imported) ``` ## Plugins ```typescript // plugins/api.ts export default defineNuxtPlugin((nuxtApp) => { const api = $fetch.create({ baseURL: '/api', onRequest({ options }) { // Add auth token const token = useCookie('token') if (token.value) { options.headers = options.headers || {} options.headers.Authorization = `Bearer ${token.value}` } }, onResponseError({ response }) { if (response.status === 401) { navigateTo('/login') } } }) return { provide: { api } } }) // Usage in component ``` ## Configuration ```typescript // nuxt.config.ts export default defineNuxtConfig({ devtools: { enabled: true }, modules: [ '@pinia/nuxt', '@nuxtjs/tailwindcss', '@vueuse/nuxt' ], runtimeConfig: { // Server-only (never exposed to client) apiSecret: process.env.API_SECRET, // Exposed to client public: { apiBase: process.env.API_BASE || '/api' } }, app: { head: { title: 'My App', meta: [ { charset: 'utf-8' }, { name: 'viewport', content: 'width=device-width, initial-scale=1' }, { name: 'description', content: 'My amazing site' } ], link: [ { rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' } ] } }, css: ['~/assets/css/main.css'], typescript: { strict: true, typeCheck: true }, // Vite is the default bundler in Nuxt 3 // Note: webpack is deprecated - use Vite for all new projects vite: { optimizeDeps: { include: ['vue', 'vue-router', 'pinia'] }, build: { rollupOptions: { output: { manualChunks: { 'vendor': ['vue', 'pinia'] } } } } }, nitro: { preset: 'vercel' // or 'node-server', 'cloudflare', 'bun', etc. } }) ``` ## SEO and Meta Tags ```vue ``` ## Custom SSR with Fastify (Non-Nuxt) For custom Vue 3 SSR without Nuxt, using Fastify as the server: ```typescript // server.ts import Fastify from 'fastify' import { createSSRApp } from 'vue' import { renderToString } from 'vue/server-renderer' import App from './App.vue' const fastify = Fastify({ logger: true }) fastify.get('*', async (request, reply) => { const app = createSSRApp(App) // Server-side data fetching const initialState = await fetchInitialData(request.url) const html = await renderToString(app) reply.type('text/html').send(` Vue SSR
${html}
`) }) fastify.listen({ port: 3000 }) ``` ```typescript // entry-client.ts import { createApp } from 'vue' import App from './App.vue' const app = createApp(App) // Hydrate with server state if (window.__INITIAL_STATE__) { app.provide('initialState', window.__INITIAL_STATE__) } app.mount('#app') ``` ```typescript // vite.config.ts for SSR import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' export default defineConfig({ plugins: [vue()], build: { ssr: true, rollupOptions: { input: { server: './server.ts', client: './src/entry-client.ts' } } } }) ``` ## Hydration Patterns ### Lazy Hydration with ClientOnly ```vue ``` ### Hydration Mismatch Prevention ```vue ``` ### Progressive Hydration ```vue ``` ```typescript // nuxt.config.ts - Configure delay hydration export default defineNuxtConfig({ modules: ['nuxt-delay-hydration'], delayHydration: { mode: 'init', // or 'mount' debug: process.env.NODE_ENV === 'development' } }) ``` ## Quick Reference | Pattern | Use Case | |---------|----------| | `useFetch()` | Fetch data (SSR-safe) | | `useAsyncData()` | Custom async operations | | `useLazyFetch()` | Non-blocking fetch | | `useState()` | Shared state across components | | `useRoute()` | Access route params/query | | `useRouter()` | Navigate programmatically | | `navigateTo()` | Navigate to route | | `definePageMeta()` | Page-level metadata | | `useHead()` | Dynamic meta tags | | Server routes | `/server/api/*.ts` | | Auto-imports | Components, composables, utils | | `` | Client-only rendering, prevent hydration mismatch | | `renderToString()` | Custom SSR with Fastify/Express | | `vite: {}` | Vite configuration in nuxt.config.ts | | `nuxt-delay-hydration` | Progressive hydration for performance |