Maîtrisez Nuxt 3 : SSR, SSG, ISR, server routes, useFetch, middleware, Nitro et déploiement fullstack Vue 3 en production.
Pourquoi Nuxt 3 ?
Les applications Vue 3 en mode SPA pur (Single Page Application) souffrent de deux problèmes fondamentaux en production : un SEO quasi inexistant (les moteurs de recherche reçoivent une page HTML vide) et un TTFB (Time To First Byte) élevé car tout le rendu se fait côté client. L'utilisateur attend que JavaScript soit téléchargé, parsé et exécuté avant de voir quoi que ce soit.
Nuxt 3 résout ces problèmes en ajoutant une couche serveur à Vue 3. C'est un meta-framework qui intègre nativement Vite pour le bundling, Nitro comme moteur serveur universel et TypeScript natif sans configuration supplémentaire. En quelques lignes, vous obtenez du rendu serveur, du routing automatique basé sur les fichiers et des auto-imports intelligents.
Comparaison des modes de rendu disponibles :
| Mode | SEO | TTFB | Fraîcheur des données | Hébergement |
|---|---|---|---|---|
| Vue SPA | Mauvais | Élevé | Temps réel | Static / CDN |
| Nuxt SSR | Excellent | Faible | Toujours fraîche | Node.js / Edge |
| Nuxt SSG | Excellent | Très faible | Au build uniquement | Static / CDN |
| Nuxt ISR | Excellent | Très faible | Revalidation périodique | Node.js / Edge |
L'ISR (Incremental Static Regeneration) combine les avantages du SSG (pages pré-générées servies depuis le cache) et du SSR (revalidation automatique après un délai configurable). C'est souvent le mode idéal pour les blogs, les e-commerces et les sites de documentation.
Installation et structure du projet
Créer un projet Nuxt 3 se fait en une seule commande avec nuxi, le CLI officiel de Nuxt. Il génère une structure de dossiers opinionée qui suit les conventions du framework.
# Créer un nouveau projet Nuxt 3
npx nuxi@latest init mon-app
# Se placer dans le dossier
cd mon-app
# Installer les dépendances
npm install
# Lancer le serveur de développement (avec HMR)
npm run dev
Structure complète du projet générée :
mon-app/
├── app.vue # Racine de l'application (point d'entrée Vue)
├── nuxt.config.ts # Configuration centrale de Nuxt
├── pages/ # Routing automatique basé sur les fichiers
│ ├── index.vue # Route → /
│ ├── about.vue # Route → /about
│ └── blog/
│ ├── index.vue # Route → /blog
│ └── [slug].vue # Route → /blog/:slug (paramètre dynamique)
├── components/ # Composants auto-importés (pas besoin d'import)
│ └── AppButton.vue # Utilisable directement dans les templates
├── composables/ # Composables auto-importés (convention use*)
│ └── useCounter.ts # Accessible via useCounter() partout
├── server/
│ ├── api/ # Routes API serveur gérées par Nitro
│ │ └── articles/
│ │ ├── index.get.ts # GET /api/articles
│ │ └── [id].get.ts # GET /api/articles/:id
│ └── middleware/ # Middleware côté serveur uniquement
├── middleware/ # Middleware de navigation (client + serveur)
│ └── auth.ts # Protège les routes nécessitant auth
├── layouts/ # Layouts réutilisables pour les pages
│ ├── default.vue # Layout par défaut
│ └── dashboard.vue # Layout admin
├── plugins/ # Plugins Nuxt (extensions de l'app)
│ └── toast.client.ts # Plugin côté client uniquement
├── public/ # Fichiers statiques servis directement
│ └── robots.txt
└── assets/ # Assets transformés par Vite (SCSS, images)
└── css/
└── main.css
Configuration de base dans nuxt.config.ts :
// nuxt.config.ts
// Fichier de configuration central de Nuxt 3 (TypeScript natif)
export default defineNuxtConfig({
// Active le mode de compatibilité pour les dernières APIs
compatibilityDate: '2024-11-01',
// Active les devtools Nuxt (onglet dans le navigateur)
devtools: { enabled: true },
// Modules Nuxt à installer (@nuxt/image, @nuxtjs/tailwindcss, etc.)
modules: [],
// Variables d'environnement accessibles côté serveur ET client
runtimeConfig: {
// Côté SERVEUR uniquement
dbConnectionString: '',
// Côté client (public)
public: {
apiBaseUrl: 'https://api.monsite.com'
}
},
// Règles de rendu par route (SSG, ISR, CSR)
routeRules: {
'/': { prerender: true }, // Page d'accueil : SSG
'/blog/**': { swr: 3600 }, // Blog : ISR avec revalidation 1h
'/dashboard/**': { ssr: false } // Dashboard : CSR (auth requise)
},
// Options du serveur de développement
devServer: {
port: 3000
}
})
components/ et les composables du dossier composables/. Vous n'avez jamais besoin d'écrire import { ref } from 'vue' — ref, computed, watch et tous les composables Nuxt (useFetch, useRoute, etc.) sont disponibles globalement.
Modes de rendu : SSR, SSG, ISR et CSR
L'un des atouts majeurs de Nuxt 3 est de pouvoir configurer le mode de rendu page par page via la propriété routeRules dans nuxt.config.ts. Vous n'êtes plus forcé de choisir un seul mode pour toute l'application.
SSR — Server-Side Rendering
Le SSR est le mode par défaut de Nuxt. Chaque requête déclenche un rendu côté serveur : le HTML complet est généré et envoyé au navigateur. Le client "hydrate" ensuite la page pour la rendre interactive. Idéal pour les pages dont le contenu change fréquemment.
SSG — Static Site Generation
Avec nuxt generate, Nuxt pré-génère toutes les pages en HTML statique au moment du build. Aucun serveur n'est nécessaire au runtime — les fichiers sont servis depuis un CDN. Parfait pour la documentation et les pages marketing.
ISR — Incremental Static Regeneration
L'ISR combine SSG et SSR : les pages sont mises en cache comme du contenu statique, mais sont automatiquement revalidées après un délai configurable (en secondes). Le premier visiteur après expiration du cache déclenche une revalidation en arrière-plan.
CSR — Client-Side Rendering
Avec ssr: false, la route se comporte comme une SPA classique Vue 3. Utile pour les pages d'administration ou de tableau de bord qui n'ont pas besoin de SEO et requièrent une authentification.
// nuxt.config.ts — configurer le rendu route par route
export default defineNuxtConfig({
routeRules: {
// Page d'accueil : générée statiquement au build (SSG)
'/': { prerender: true },
// Pages blog : ISR avec revalidation automatique toutes les heures
// swr = stale-while-revalidate (standard HTTP)
'/blog/**': { swr: 3600 },
// Fiches produits : ISR avec revalidation toutes les 10 minutes
'/produits/**': { swr: 600 },
// Dashboard : rendu côté client uniquement (pas de SSR)
// Évite d'exposer des données sensibles côté serveur
'/dashboard/**': { ssr: false },
// API routes : activer CORS pour les appels cross-origin
'/api/**': { cors: true }
}
})
swr est souvent le meilleur compromis — performances SSG (cache CDN) + fraîcheur contrôlable. Utilisez prerender: true uniquement pour les pages dont le contenu ne change jamais ou rarement.
Vous pouvez également configurer le mode de rendu directement dans une page Vue avec definePageMeta :
<!-- pages/blog/[slug].vue -->
<script setup lang="ts">
// definePageMeta : métadonnées de page (layout, middleware, rendu)
definePageMeta({
// Surcharge locale de routeRules pour cette page uniquement
// Revalidation toutes les 30 minutes pour les articles de blog
})
</script>
useFetch et useAsyncData
Nuxt 3 fournit deux composables SSR-aware pour récupérer des données : useFetch et useAsyncData. Contrairement à un fetch classique, ces composables sont exécutés côté serveur lors du premier rendu, puis côté client pour les navigations suivantes. Les données sont sérialisées et hydratées automatiquement — pas de double requête.
useFetch — le composable tout-en-un
useFetch est un wrapper de haut niveau autour de useAsyncData et de $fetch. Il est optimisé pour les cas courants : appel d'une URL, gestion des états de chargement et d'erreur, et mise en cache automatique.
<!-- pages/blog/index.vue -->
<script setup lang="ts">
// Interface TypeScript pour typer les données reçues
interface Article {
id: number
title: string
body: string
userId: number
}
// useFetch : auto-importé par Nuxt, exécuté côté serveur au premier rendu
// pending : true pendant le chargement
// error : contient l'erreur HTTP si échec
// refresh : fonction pour relancer le fetch manuellement
const { data: articles, pending, error, refresh } = await useFetch<Article[]>(
'https://jsonplaceholder.typicode.com/posts',
{
// transform : modifie les données avant hydratation côté client
// Évite de stocker des données inutiles dans le payload SSR
transform: (data) => data.slice(0, 10),
// pick : sélectionne uniquement certains champs (réduit le payload)
// pick: ['id', 'title'],
// lazy : si true, ne bloque pas le rendu (affiche le skeleton d'abord)
lazy: false,
// headers : envoyer des headers personnalisés (ex: Authorization)
headers: {
'Accept': 'application/json'
},
// watch : re-déclenche le fetch si ces refs changent
// watch: [page, limit]
}
)
</script>
<template>
<div>
<!-- Afficher un loader pendant le chargement -->
<div v-if="pending" class="text-center">Chargement des articles...</div>
<!-- Afficher l'erreur si la requête a échoué -->
<div v-else-if="error" class="alert alert-danger">
Erreur : {{ error.message }}
</div>
<!-- Afficher la liste des articles -->
<ul v-else class="list-unstyled">
<li v-for="article in articles" :key="article.id" class="mb-3">
<h3>{{ article.title }}</h3>
<p>{{ article.body }}</p>
</li>
</ul>
<!-- Bouton pour rafraîchir les données sans recharger la page -->
<button @click="refresh()" class="btn btn-primary">Actualiser</button>
</div>
</template>
useAsyncData — contrôle total
useAsyncData offre plus de flexibilité quand vous avez besoin d'appeler plusieurs sources de données simultanément, d'accéder à une base de données directement ou d'implémenter une logique de fetching personnalisée.
<!-- pages/profil.vue -->
<script setup lang="ts">
const route = useRoute() // Accès aux paramètres de la route courante
// useAsyncData : premier argument = clé unique pour le cache
// La clé doit être unique dans l'application pour éviter les conflits
const { data: userProfile } = await useAsyncData(
`user-${route.params.id}`, // Clé dynamique incluant l'ID de la route
async () => {
// Logique async personnalisée : appels parallèles avec Promise.all
const [profile, stats, recentActivity] = await Promise.all([
$fetch(`/api/users/${route.params.id}`),
$fetch(`/api/users/${route.params.id}/stats`),
$fetch(`/api/users/${route.params.id}/activity?limit=5`)
])
// Retourner les données combinées — sérialisées dans le payload SSR
return {
...profile,
stats,
recentActivity
}
},
{
// watch : re-exécute la fonction si route.params.id change
// Utile pour les pages avec paramètres dynamiques
watch: [() => route.params.id],
// default : valeur par défaut avant que les données soient disponibles
default: () => ({ name: '', stats: {}, recentActivity: [] })
}
)
</script>
Tableau comparatif useFetch vs useAsyncData :
| Critère | useFetch | useAsyncData |
|---|---|---|
| Cas d'usage principal | Appel d'une URL | Logique async complexe |
| Sources multiples | Non (une URL) | Oui (Promise.all) |
| Déduplication | Automatique | Via la clé manuelle |
| Options de fetch | Headers, body, method... | Liberté totale |
| Verbosité | Faible | Moyenne |
useFetch pour les cas simples. Passez à useAsyncData uniquement quand vous avez besoin de combiner plusieurs sources ou d'accéder à des ressources qui ne sont pas des URLs HTTP (BDD, cache Redis, système de fichiers via une fonction Nitro).
Server Routes avec Nitro
Nitro est le moteur serveur qui propulse Nuxt 3. Il permet de créer des routes API dans le dossier server/api/ qui sont compilées en handlers ultra-légers, déployables sur n'importe quel runtime : Node.js, Bun, Deno, Cloudflare Workers, Vercel Edge Functions et plus encore.
La convention de nommage est simple et expressive : le nom du fichier détermine la méthode HTTP (index.get.ts = GET, index.post.ts = POST) et le routing ([id].get.ts = route dynamique).
Route GET avec pagination
// server/api/articles/index.get.ts
// Nitro détecte automatiquement la méthode HTTP depuis le nom du fichier
export default defineEventHandler(async (event) => {
// getQuery : récupère les query params de l'URL (?page=1&limit=10)
const query = getQuery(event)
const page = Number(query.page) || 1
const limit = Number(query.limit) || 10
// Calculer l'offset pour la pagination
const offset = (page - 1) * limit
// Ici : appeler votre BDD (Prisma, Drizzle, mysql2, etc.)
// Exemple simplifié avec des données en dur
const allArticles = [
{ id: 1, title: 'Premier article', slug: 'premier-article' },
{ id: 2, title: 'Deuxième article', slug: 'deuxieme-article' },
]
const articles = allArticles.slice(offset, offset + limit)
// setResponseHeader : configurer les headers de la réponse HTTP
setResponseHeader(event, 'Cache-Control', 'max-age=60, s-maxage=3600')
// Le retour est automatiquement sérialisé en JSON
return {
data: articles,
meta: {
page,
limit,
total: allArticles.length,
totalPages: Math.ceil(allArticles.length / limit)
}
}
})
Route GET dynamique avec validation
// server/api/articles/[id].get.ts
// [id] dans le nom = paramètre dynamique accessible via event.context.params.id
export default defineEventHandler(async (event) => {
// getRouterParam : récupère un paramètre de route de façon sécurisée
const idParam = getRouterParam(event, 'id')
// Valider manuellement le paramètre (ou utiliser Zod/Valibot)
const id = Number(idParam)
if (isNaN(id) || id <= 0) {
// createError : génère une erreur HTTP standardisée
// statusCode et message sont renvoyés au client en JSON
throw createError({
statusCode: 400,
statusMessage: 'ID invalide — doit être un entier positif'
})
}
// Simuler un appel BDD — remplacer par Prisma/Drizzle/etc.
const article = id === 1
? { id: 1, title: 'Premier article', content: 'Contenu...' }
: null
// Si l'article n'existe pas, retourner 404
if (!article) {
throw createError({
statusCode: 404,
statusMessage: `Article avec l'ID ${id} introuvable`
})
}
return article
})
Route POST avec validation du body
// server/api/articles/index.post.ts
// Gestion de la création d'un article (méthode POST)
export default defineEventHandler(async (event) => {
// readBody : lit et parse automatiquement le corps JSON de la requête
const body = await readBody(event)
// Validation manuelle du body (alternative : utiliser zod ou valibot)
if (!body.title || typeof body.title !== 'string' || body.title.length < 3) {
throw createError({
statusCode: 422,
statusMessage: 'Le titre est obligatoire (minimum 3 caractères)'
})
}
if (!body.content || typeof body.content !== 'string') {
throw createError({
statusCode: 422,
statusMessage: 'Le contenu est obligatoire'
})
}
// Simuler la création en BDD (remplacer par votre ORM)
const newArticle = {
id: Date.now(), // ID généré (en production : BDD auto-increment)
title: body.title.trim(),
content: body.content,
tags: body.tags ?? [],
createdAt: new Date().toISOString()
}
// setResponseStatus : code HTTP 201 Created (ressource créée avec succès)
setResponseStatus(event, 201)
return newArticle
})
Appeler les server routes depuis un composant
<!-- Appel depuis un composant Vue — useFetch vers une route Nitro -->
<script setup lang="ts">
// $fetch : helper Nuxt pour les appels HTTP sans gestion SSR
// À utiliser dans les event handlers (clicks, submit) — pas au montage
const createArticle = async () => {
try {
// POST vers la route Nitro server/api/articles/index.post.ts
const result = await $fetch('/api/articles', {
method: 'POST',
body: {
title: 'Mon nouvel article',
content: 'Contenu de l\'article...',
tags: ['vue3', 'nuxt3']
}
})
console.log('Article créé :', result)
} catch (error: any) {
// error.data contient le message d'erreur de createError()
console.error('Erreur :', error.data?.statusMessage)
}
}
</script>
Middleware et plugins
Nuxt 3 propose deux systèmes de middleware distincts : les middleware de navigation (dans middleware/) qui s'exécutent avant chaque changement de route côté client et serveur, et les middleware serveur (dans server/middleware/) qui interceptent chaque requête HTTP entrante.
Middleware de navigation — protection des routes
// middleware/auth.ts
// Ce middleware s'exécute avant chaque navigation vers une route protégée
export default defineNuxtRouteMiddleware((to, from) => {
// useCookie : accès aux cookies côté serveur ET client (SSR-safe)
// Contrairement à localStorage qui n'est disponible que côté client
const token = useCookie('auth_token')
// Si aucun token n'est présent, rediriger vers la page de connexion
if (!token.value) {
// navigateTo : helper Nuxt pour les redirections (fonctionne SSR + client)
// replace: true = pas d'entrée dans l'historique du navigateur
return navigateTo('/login', { replace: true })
}
// Vérifier les métadonnées de la route cible
// to.meta.requiredRole est défini via definePageMeta dans la page
if (to.meta.requiredRole && to.meta.requiredRole !== getUserRole(token.value)) {
// Lever une erreur 403 si le rôle ne correspond pas
throw createError({ statusCode: 403, statusMessage: 'Accès interdit' })
}
})
// Fonction utilitaire pour extraire le rôle depuis le token JWT
function getUserRole(token: string): string {
try {
// Décode le payload JWT (sans vérification de signature côté client)
const payload = JSON.parse(atob(token.split('.')[1]))
return payload.role || 'user'
} catch {
return 'user'
}
}
Appliquer le middleware sur une page :
<!-- pages/dashboard/index.vue -->
<script setup lang="ts">
// definePageMeta : déclare les métadonnées de la page
// Exécuté au moment de la compilation, pas au runtime
definePageMeta({
// Applique le middleware 'auth' avant d'afficher cette page
// Correspond au fichier middleware/auth.ts
middleware: ['auth'],
// Utilise le layout 'dashboard' (layouts/dashboard.vue)
layout: 'dashboard',
// Métadonnée personnalisée lisible dans le middleware
requiredRole: 'admin'
})
</script>
Middleware global — analytics et logging
// middleware/analytics.global.ts
// Le suffixe .global.ts = s'exécute sur TOUTES les routes automatiquement
// Pas besoin de l'ajouter dans definePageMeta
export default defineNuxtRouteMiddleware((to, from) => {
// Ignorer la première navigation (from.name est null au chargement initial)
if (!from.name) return
// Envoyer un événement de tracking à chaque changement de page
// Utiliser $fetch en background — ne pas attendre la réponse
$fetch('/api/analytics/pageview', {
method: 'POST',
body: {
path: to.path,
referrer: from.path,
timestamp: Date.now()
}
}).catch(() => {
// Ignorer les erreurs analytics — ne pas bloquer la navigation
})
})
Plugins Nuxt — extensions globales
// plugins/toast.client.ts
// Convention de nommage :
// .client.ts → exécuté UNIQUEMENT côté client (navigateur)
// .server.ts → exécuté UNIQUEMENT côté serveur
// Sans suffixe → exécuté côté serveur ET client
export default defineNuxtPlugin((nuxtApp) => {
// Injecter un helper $toast accessible dans tous les composants
// Via useNuxtApp().$toast ou inject('toast') en Options API
return {
provide: {
// Fonction toast simple (adaptable avec une vraie lib comme vue-toastification)
toast: (message: string, type: 'success' | 'error' | 'info' = 'info') => {
// Créer une div de notification et l'ajouter au DOM
const el = document.createElement('div')
el.className = `toast-notification toast-${type}`
el.textContent = message
document.body.appendChild(el)
// Supprimer automatiquement après 3 secondes
setTimeout(() => el.remove(), 3000)
}
}
}
})
Utiliser le plugin dans un composant :
<script setup lang="ts">
// useNuxtApp : accès à l'instance Nuxt et aux plugins injectés
const { $toast } = useNuxtApp()
const handleSubmit = async () => {
try {
await $fetch('/api/articles', { method: 'POST', body: formData })
// Appel du plugin toast injecté
$toast('Article créé avec succès !', 'success')
} catch {
$toast('Une erreur est survenue', 'error')
}
}
</script>
.global.ts) s'exécute sur TOUTES les routes — réservez-le aux opérations légères (analytics, logging). Pour les vérifications d'auth, utilisez des middlewares nommés et appliquez-les explicitement via definePageMeta.
useHead et SEO
Nuxt 3 fournit des composables dédiés à la gestion du <head> HTML : useHead pour le contrôle complet et useSeoMeta pour une API typée et plus concise. Ces composables sont réactifs — les meta tags se mettent à jour automatiquement quand les données changent.
useHead — gestion complète du head
<!-- pages/blog/[slug].vue -->
<script setup lang="ts">
const route = useRoute()
// Récupérer les données de l'article (SSR)
const { data: article } = await useFetch(`/api/articles/${route.params.slug}`)
// useHead : contrôle total sur toutes les balises du <head>
// Les fonctions fléchées () => ... rendent les valeurs réactives
useHead({
// Titre de la page — affiché dans l'onglet du navigateur
title: () => article.value ? `${article.value.title} | Mon Blog` : 'Chargement...',
// Meta tags standards
meta: [
// Description pour les moteurs de recherche (max 160 caractères)
{ name: 'description', content: () => article.value?.excerpt ?? '' },
// Open Graph — contrôle l'aperçu lors du partage sur les réseaux sociaux
{ property: 'og:title', content: () => article.value?.title ?? '' },
{ property: 'og:description', content: () => article.value?.excerpt ?? '' },
{ property: 'og:image', content: () => article.value?.coverImage ?? '' },
{ property: 'og:type', content: 'article' },
{ property: 'og:locale', content: 'fr_FR' },
// Twitter Card — aperçu sur Twitter/X
{ name: 'twitter:card', content: 'summary_large_image' },
{ name: 'twitter:title', content: () => article.value?.title ?? '' },
{ name: 'twitter:description', content: () => article.value?.excerpt ?? '' },
{ name: 'twitter:image', content: () => article.value?.coverImage ?? '' },
// Robots — contrôle l'indexation par les moteurs de recherche
{ name: 'robots', content: 'index, follow' }
],
link: [
// URL canonique — évite le contenu dupliqué si la page est accessible via plusieurs URLs
{ rel: 'canonical', href: () => `https://monsite.com/blog/${route.params.slug}` }
],
// Script JSON-LD — rich snippets Google (étoiles, dates, auteur)
script: [
{
type: 'application/ld+json',
children: () => JSON.stringify({
'@context': 'https://schema.org',
'@type': 'Article',
headline: article.value?.title,
datePublished: article.value?.publishedAt,
author: { '@type': 'Person', name: article.value?.author }
})
}
]
})
</script>
useSeoMeta — API typée et concise
useSeoMeta est un raccourci avec autocomplétion TypeScript complète. Il couvre les tags Open Graph, Twitter Card et les meta standards sans avoir à se souvenir des noms exacts des propriétés.
<!-- pages/index.vue — page d'accueil -->
<script setup lang="ts">
// useSeoMeta : API typée avec autocomplétion complète dans VSCode
// Idéal pour les pages statiques où les meta ne changent pas
useSeoMeta({
// Titre de la page (balise <title>)
title: 'Accueil | Mon Blog Vue 3',
// Meta description pour Google
description: 'Découvrez nos articles techniques sur Vue 3, Nuxt 3 et le développement web moderne.',
// Open Graph (Facebook, LinkedIn, Discord...)
ogTitle: 'Accueil | Mon Blog Vue 3',
ogDescription: 'Articles techniques sur Vue 3 et Nuxt 3.',
ogImage: 'https://monsite.com/images/og-home.jpg',
ogUrl: 'https://monsite.com',
ogType: 'website',
ogLocale: 'fr_FR',
// Twitter Card
twitterCard: 'summary_large_image',
twitterTitle: 'Accueil | Mon Blog Vue 3',
twitterDescription: 'Articles techniques sur Vue 3 et Nuxt 3.',
twitterImage: 'https://monsite.com/images/og-home.jpg',
// Directive robots
robots: 'index, follow'
})
</script>
useHead dans app.vue pour les meta globales (charset, viewport, favicons) et useSeoMeta dans chaque page pour les meta spécifiques. Les meta définies dans les pages ont la priorité sur celles définies dans les layouts et app.vue.
Déploiement et configuration
Nuxt 3 utilise runtimeConfig pour gérer les variables d'environnement de façon sécurisée : les secrets restent côté serveur et ne sont jamais exposés au bundle JavaScript du client.
Configuration des variables d'environnement
// nuxt.config.ts — configuration des variables d'environnement
export default defineNuxtConfig({
runtimeConfig: {
// Ces variables sont UNIQUEMENT disponibles côté serveur
// Elles ne font JAMAIS partie du bundle JavaScript client
dbConnectionString: process.env.DATABASE_URL ?? '',
apiSecretKey: process.env.API_SECRET_KEY ?? '',
jwtSecret: process.env.JWT_SECRET ?? '',
// public : variables accessibles côté client ET serveur
// Ne jamais y mettre de secrets !
public: {
apiBaseUrl: process.env.NUXT_PUBLIC_API_URL ?? 'http://localhost:3000',
analyticsId: process.env.NUXT_PUBLIC_ANALYTICS_ID ?? '',
appVersion: process.env.npm_package_version ?? '1.0.0'
}
}
})
# .env — variables locales (ne JAMAIS commiter ce fichier)
# Les variables NUXT_PUBLIC_* sont automatiquement mappées sur runtimeConfig.public.*
# Les variables NUXT_* (sans PUBLIC) sont mappées sur runtimeConfig.*
DATABASE_URL=postgresql://user:password@localhost:5432/mydb
API_SECRET_KEY=mon-secret-tres-long-et-aleatoire
JWT_SECRET=autre-secret-jwt
NUXT_PUBLIC_API_URL=http://localhost:3000
NUXT_PUBLIC_ANALYTICS_ID=UA-XXXXXXXXX-1
// server/api/users/index.get.ts — utiliser runtimeConfig côté serveur
export default defineEventHandler(async (event) => {
// useRuntimeConfig dans le serveur : accès à TOUTES les variables
const config = useRuntimeConfig(event)
// config.dbConnectionString est disponible ici (côté serveur)
// En production, ce fichier ne tourne que sur Node.js — jamais dans le navigateur
console.log('Connexion à :', config.dbConnectionString)
// Simuler une requête BDD
return { users: [] }
})
<!-- Composant Vue — utiliser runtimeConfig côté client -->
<script setup lang="ts">
// useRuntimeConfig côté client : UNIQUEMENT config.public.*
// config.dbConnectionString = undefined (jamais exposé au client)
const config = useRuntimeConfig()
// Utiliser l'URL de base pour les appels API
const { data } = await useFetch(`${config.public.apiBaseUrl}/articles`)
</script>
Build et déploiement
# Build standard pour Node.js (preset par défaut)
npx nuxi build
# Lancer le serveur en production
node .output/server/index.mjs
# Passer les variables d'environnement au démarrage
DATABASE_URL="postgresql://..." JWT_SECRET="..." node .output/server/index.mjs
# Générer un site statique complet (SSG)
npx nuxi generate
# Les fichiers statiques sont dans .output/public/
# Déployable sur n'importe quel hébergeur statique (Netlify, GitHub Pages...)
# Déploiement sur Vercel (auto-détection Nuxt 3)
npm install -g vercel
vercel
# Déploiement sur Cloudflare Pages (Edge Runtime)
# Nitro génère un bundle optimisé pour Cloudflare Workers
NITRO_PRESET=cloudflare-pages npx nuxi build
# Déploiement sur Netlify
NITRO_PRESET=netlify npx nuxi build
# Docker — créer une image Node.js
# Le .output/ est autosuffisant (aucun node_modules requis)
FROM node:20-alpine
WORKDIR /app
COPY .output/ .
EXPOSE 3000
CMD ["node", "server/index.mjs"]
Checklist Nuxt 3 production
Avant de déployer une application Nuxt 3 en production, vérifiez chacun de ces points pour garantir performance, sécurité et maintenabilité.
-
nuxt.config.ts:routeRulesconfigurés selon le mode de rendu optimal pour chaque groupe de routes (SSG/ISR/CSR) -
runtimeConfig: secrets serveur isolés dans les clés non-public, variables client danspublic - Server routes validées avec validation manuelle ou Zod (paramètres, query params et body)
- Middleware d'auth appliqué sur toutes les routes protégées via
definePageMeta -
useSeoMetaouuseHeadconfiguré sur chaque page avec title, description, og:image et canonical -
useFetch: optiontransformutilisée pour réduire le payload SSR aux données nécessaires - Page
error.vuecréée à la racine pour une gestion d'erreurs personnalisée (404, 500, etc.) - Preset Nitro adapté à l'hébergement cible (node, vercel, cloudflare-pages, netlify)
- Variables d'env
.envdocumentées dans.env.exampleet jamais commitées -
nuxt generatetesté et validé pour les pages en mode SSG (prerender: true) - Absence de mismatch SSR/CSR (warning dans la console) — vérifier les composants avec accès au DOM
- Lighthouse score > 90 avec TTFB < 200ms en SSR et Core Web Vitals dans le vert
Tableau récapitulatif des APIs principales de Nuxt 3 :
| Feature | API Nuxt 3 | Description |
|---|---|---|
| Fetch SSR | useFetch / useAsyncData |
Données hydratées automatiquement côté client |
| Routes API | server/api/*.ts |
Handlers Nitro (GET, POST, PUT, DELETE) |
| Auth | middleware/auth.ts |
defineNuxtRouteMiddleware + useCookie |
| SEO | useHead / useSeoMeta |
Meta tags réactifs avec autocomplétion TypeScript |
| Config | runtimeConfig |
Secrets serveur isolés du bundle client |
| Rendu | routeRules |
SSR / SSG / ISR / CSR par route |
| Plugins | plugins/*.ts |
Extensions globales (.client / .server) |
| Deploy | nuxi build |
Node / Edge / Serverless via preset Nitro |
Conclusion
Nuxt 3 transforme Vue 3 en une plateforme fullstack complète, capable de répondre à des besoins allant du simple site statique à l'application d'entreprise avec API intégrée. La combinaison Nitro + routeRules permet de mixer SSR, SSG et ISR sur la même application sans sacrifier la cohérence de l'architecture.
Les composables SSR-aware (useFetch, useAsyncData), le système de middleware, la gestion des variables d'environnement via runtimeConfig et les server routes Nitro constituent un socle robuste pour construire des applications Vue 3 prêtes pour la production. Commencez par maîtriser useFetch et les server routes, puis ajoutez progressivement le SEO avec useSeoMeta et les middlewares d'auth selon les besoins de votre projet.