Next.js 15 : App Router, Server Actions et les Nouvelles Fonctionnalités à Connaître
Guide complet sur Next.js 15 : App Router optimisé, Server Actions, Turbopack stable, cache intelligent et nouvelles API. Exemples pratiques pour votre prochain projet.
title: "Next.js 15 : App Router, Server Actions et les Nouvelles Fonctionnalités à Connaître" description: "Guide complet sur Next.js 15 : App Router optimisé, Server Actions, Turbopack stable, cache intelligent et nouvelles API. Exemples pratiques pour votre prochain projet." date: "2025-12-05" author: name: "Mustapha Hamadi" role: "Développeur Full-Stack" image: "/avatar.jpg" tags: ["Next.js", "React", "Full-Stack"] category: "development" image: "/blog/nextjs-15-app-router-server-actions-guide-complet-hero.svg" ogImage: "/blog/nextjs-15-app-router-server-actions-guide-complet-hero.svg" featured: false published: true keywords: ["Next.js 15", "App Router", "Server Actions", "Turbopack", "React Server Components", "cache Next.js", "développement full-stack", "framework React", "SSR", "ISR", "Static Generation", "API Routes"]
Next.js 15 : App Router, Server Actions et les Nouvelles Fonctionnalités à Connaître
Next.js 15 consolide la révolution initiée avec l'App Router et apporte des améliorations majeures en termes de performance, stabilité et expérience développeur. Cette version marque la maturité du framework avec Turbopack stable, des Server Actions optimisées et un système de cache repensé. Explorons ensemble chaque nouveauté avec des exemples concrets.
Turbopack : Le Bundler Nouvelle Génération Stabilisé
Performance de Développement Exceptionnelle
Turbopack, le successeur de Webpack développé en Rust, est désormais stable pour le développement. Les gains de performance sont spectaculaires.
# Activer Turbopack (maintenant stable)
next dev --turbo
# Comparaison des performances
# Webpack : ~3.5s pour le démarrage initial
# Turbopack : ~400ms pour le démarrage initial
Améliorations mesurées :
- Démarrage du serveur de développement : jusqu'à 76% plus rapide
- Fast Refresh : jusqu'à 96% plus rapide
- Compilation initiale des routes : jusqu'à 45% plus rapide
Configuration Optimisée
// next.config.ts
import type { NextConfig } from 'next';
const config: NextConfig = {
// Turbopack est activé automatiquement avec --turbo
// Configuration spécifique si nécessaire
experimental: {
turbo: {
rules: {
'*.svg': {
loaders: ['@svgr/webpack'],
as: '*.js',
},
},
},
},
};
export default config;
Server Actions : Mutations Simplifiées
Anatomie d'une Server Action
Les Server Actions permettent d'exécuter du code serveur directement depuis vos composants, sans créer d'API endpoints séparés.
// app/actions/user.ts
'use server';
import { db } from '@/lib/database';
import { revalidatePath } from 'next/cache';
import { redirect } from 'next/navigation';
import { z } from 'zod';
const userSchema = z.object({
name: z.string().min(2, 'Le nom doit contenir au moins 2 caractères'),
email: z.string().email('Email invalide'),
role: z.enum(['user', 'admin', 'editor']),
});
export async function createUser(formData: FormData) {
// Validation des données
const validatedFields = userSchema.safeParse({
name: formData.get('name'),
email: formData.get('email'),
role: formData.get('role'),
});
if (!validatedFields.success) {
return {
success: false,
errors: validatedFields.error.flatten().fieldErrors,
};
}
// Création en base de données
const user = await db.user.create({
data: validatedFields.data,
});
// Revalidation du cache
revalidatePath('/users');
// Redirection après succès
redirect(`/users/${user.id}`);
}
Utilisation dans un Formulaire
// app/users/new/page.tsx
import { createUser } from '@/app/actions/user';
export default function NewUserPage() {
return (
<div className="max-w-md mx-auto p-6">
<h1 className="text-2xl font-bold mb-6">Nouvel Utilisateur</h1>
<form action={createUser} className="space-y-4">
<div>
<label htmlFor="name" className="block text-sm font-medium">
Nom
</label>
<input
type="text"
id="name"
name="name"
required
className="mt-1 block w-full rounded-md border p-2"
/>
</div>
<div>
<label htmlFor="email" className="block text-sm font-medium">
Email
</label>
<input
type="email"
id="email"
name="email"
required
className="mt-1 block w-full rounded-md border p-2"
/>
</div>
<div>
<label htmlFor="role" className="block text-sm font-medium">
Rôle
</label>
<select
id="role"
name="role"
className="mt-1 block w-full rounded-md border p-2"
>
<option value="user">Utilisateur</option>
<option value="editor">Éditeur</option>
<option value="admin">Administrateur</option>
</select>
</div>
<SubmitButton />
</form>
</div>
);
}
Composant de Soumission avec État
'use client';
import { useFormStatus } from 'react-dom';
export function SubmitButton() {
const { pending } = useFormStatus();
return (
<button
type="submit"
disabled={pending}
className="w-full bg-blue-600 text-white py-2 px-4 rounded-md
hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed
transition-colors"
>
{pending ? (
<span className="flex items-center justify-center gap-2">
<Spinner /> Création en cours...
</span>
) : (
'Créer l\'utilisateur'
)}
</button>
);
}
function Spinner() {
return (
<svg className="animate-spin h-5 w-5" viewBox="0 0 24 24">
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
fill="none"
/>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
/>
</svg>
);
}
Nouveau Système de Cache
Comportement par Défaut Modifié
Next.js 15 change le comportement par défaut du cache pour plus de prévisibilité.
// Avant Next.js 15 : Cache par défaut
// Après Next.js 15 : Pas de cache par défaut
// Pour activer le cache explicitement
export const dynamic = 'force-static';
// Ou au niveau de la requête fetch
const data = await fetch('https://api.exemple.com/data', {
next: { revalidate: 3600 } // Cache pendant 1 heure
});
Configuration Granulaire du Cache
// app/products/page.tsx
import { unstable_cache } from 'next/cache';
// Cache avec tags pour invalidation ciblée
const getProducts = unstable_cache(
async (category: string) => {
const products = await db.product.findMany({
where: { category },
orderBy: { createdAt: 'desc' },
});
return products;
},
['products'], // Clé de cache
{
tags: ['products', 'catalog'],
revalidate: 3600, // 1 heure
}
);
export default async function ProductsPage({
searchParams,
}: {
searchParams: { category?: string };
}) {
const products = await getProducts(searchParams.category ?? 'all');
return (
<div className="grid grid-cols-3 gap-4">
{products.map(product => (
<ProductCard key={product.id} product={product} />
))}
</div>
);
}
Invalidation par Tags
// app/actions/product.ts
'use server';
import { revalidateTag } from 'next/cache';
export async function updateProduct(id: string, data: ProductData) {
await db.product.update({
where: { id },
data,
});
// Invalide tous les caches avec ce tag
revalidateTag('products');
}
export async function deleteProduct(id: string) {
await db.product.delete({ where: { id } });
// Invalidation multiple
revalidateTag('products');
revalidateTag('catalog');
}
App Router : Patterns Avancés
Layouts Imbriqués
// app/dashboard/layout.tsx
import { Sidebar } from '@/components/dashboard/sidebar';
import { Header } from '@/components/dashboard/header';
export default function DashboardLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div className="min-h-screen flex">
<Sidebar />
<div className="flex-1 flex flex-col">
<Header />
<main className="flex-1 p-6 bg-gray-50">
{children}
</main>
</div>
</div>
);
}
Parallel Routes
Les routes parallèles permettent d'afficher plusieurs pages simultanément.
app/
├── dashboard/
│ ├── @analytics/
│ │ └── page.tsx
│ ├── @notifications/
│ │ └── page.tsx
│ ├── layout.tsx
│ └── page.tsx
// app/dashboard/layout.tsx
export default function DashboardLayout({
children,
analytics,
notifications,
}: {
children: React.ReactNode;
analytics: React.ReactNode;
notifications: React.ReactNode;
}) {
return (
<div className="grid grid-cols-12 gap-4">
<div className="col-span-8">
{children}
</div>
<div className="col-span-4 space-y-4">
{analytics}
{notifications}
</div>
</div>
);
}
Intercepting Routes
Créez des modales qui préservent le contexte de navigation.
app/
├── photos/
│ └── [id]/
│ └── page.tsx # Page complète
├── @modal/
│ └── (.)photos/
│ └── [id]/
│ └── page.tsx # Version modale
└── layout.tsx
// app/@modal/(.)photos/[id]/page.tsx
import { Modal } from '@/components/ui/modal';
import { getPhoto } from '@/lib/photos';
export default async function PhotoModal({
params,
}: {
params: { id: string };
}) {
const photo = await getPhoto(params.id);
return (
<Modal>
<img src={photo.url} alt={photo.title} className="w-full" />
<h2 className="text-xl font-bold mt-4">{photo.title}</h2>
<p className="text-gray-600">{photo.description}</p>
</Modal>
);
}
Streaming et Suspense
Chargement Progressif
// app/dashboard/page.tsx
import { Suspense } from 'react';
import { StatsCards } from '@/components/dashboard/stats-cards';
import { RecentOrders } from '@/components/dashboard/recent-orders';
import { SalesChart } from '@/components/dashboard/sales-chart';
export default function DashboardPage() {
return (
<div className="space-y-6">
<h1 className="text-2xl font-bold">Tableau de bord</h1>
{/* Statistiques - chargées en premier */}
<Suspense fallback={<StatsCardsSkeleton />}>
<StatsCards />
</Suspense>
<div className="grid grid-cols-2 gap-6">
{/* Graphique - peut prendre plus de temps */}
<Suspense fallback={<ChartSkeleton />}>
<SalesChart />
</Suspense>
{/* Commandes récentes - indépendant */}
<Suspense fallback={<OrdersSkeleton />}>
<RecentOrders />
</Suspense>
</div>
</div>
);
}
Loading UI Automatique
// app/dashboard/loading.tsx
export default function DashboardLoading() {
return (
<div className="animate-pulse space-y-6">
<div className="h-8 w-48 bg-gray-200 rounded" />
<div className="grid grid-cols-4 gap-4">
{[...Array(4)].map((_, i) => (
<div key={i} className="h-24 bg-gray-200 rounded-lg" />
))}
</div>
<div className="h-64 bg-gray-200 rounded-lg" />
</div>
);
}
Middleware Amélioré
Protection des Routes
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { getToken } from 'next-auth/jwt';
export async function middleware(request: NextRequest) {
const token = await getToken({ req: request });
const isAuthenticated = !!token;
// Routes protégées
const protectedPaths = ['/dashboard', '/settings', '/profile'];
const isProtectedRoute = protectedPaths.some(path =>
request.nextUrl.pathname.startsWith(path)
);
if (isProtectedRoute && !isAuthenticated) {
const loginUrl = new URL('/login', request.url);
loginUrl.searchParams.set('callbackUrl', request.nextUrl.pathname);
return NextResponse.redirect(loginUrl);
}
// Redirection si déjà connecté
if (request.nextUrl.pathname === '/login' && isAuthenticated) {
return NextResponse.redirect(new URL('/dashboard', request.url));
}
return NextResponse.next();
}
export const config = {
matcher: ['/dashboard/:path*', '/settings/:path*', '/profile/:path*', '/login'],
};
Headers et Géolocalisation
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
const response = NextResponse.next();
// Ajouter des headers de sécurité
response.headers.set('X-Frame-Options', 'DENY');
response.headers.set('X-Content-Type-Options', 'nosniff');
response.headers.set('Referrer-Policy', 'origin-when-cross-origin');
// Géolocalisation pour personnalisation
const country = request.geo?.country ?? 'FR';
const city = request.geo?.city ?? 'Unknown';
response.headers.set('X-User-Country', country);
response.headers.set('X-User-City', city);
return response;
}
API Routes Nouvelle Génération
Route Handlers
// app/api/users/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { db } from '@/lib/database';
import { z } from 'zod';
const userSchema = z.object({
name: z.string().min(2),
email: z.string().email(),
});
// GET /api/users
export async function GET(request: NextRequest) {
const searchParams = request.nextUrl.searchParams;
const page = parseInt(searchParams.get('page') ?? '1');
const limit = parseInt(searchParams.get('limit') ?? '10');
const users = await db.user.findMany({
skip: (page - 1) * limit,
take: limit,
orderBy: { createdAt: 'desc' },
});
const total = await db.user.count();
return NextResponse.json({
users,
pagination: {
page,
limit,
total,
totalPages: Math.ceil(total / limit),
},
});
}
// POST /api/users
export async function POST(request: NextRequest) {
try {
const body = await request.json();
const validatedData = userSchema.parse(body);
const user = await db.user.create({
data: validatedData,
});
return NextResponse.json(user, { status: 201 });
} catch (error) {
if (error instanceof z.ZodError) {
return NextResponse.json(
{ errors: error.errors },
{ status: 400 }
);
}
return NextResponse.json(
{ error: 'Erreur interne du serveur' },
{ status: 500 }
);
}
}
Routes Dynamiques
// app/api/users/[id]/route.ts
import { NextRequest, NextResponse } from 'next/server';
export async function GET(
request: NextRequest,
{ params }: { params: { id: string } }
) {
const user = await db.user.findUnique({
where: { id: params.id },
});
if (!user) {
return NextResponse.json(
{ error: 'Utilisateur non trouvé' },
{ status: 404 }
);
}
return NextResponse.json(user);
}
export async function PUT(
request: NextRequest,
{ params }: { params: { id: string } }
) {
const body = await request.json();
const user = await db.user.update({
where: { id: params.id },
data: body,
});
return NextResponse.json(user);
}
export async function DELETE(
request: NextRequest,
{ params }: { params: { id: string } }
) {
await db.user.delete({
where: { id: params.id },
});
return new NextResponse(null, { status: 204 });
}
Optimisations d'Images et Fonts
Image Component Optimisé
import Image from 'next/image';
export function ProductImage({ product }: { product: Product }) {
return (
<div className="relative aspect-square overflow-hidden rounded-lg">
<Image
src={product.imageUrl}
alt={product.name}
fill
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
className="object-cover transition-transform hover:scale-105"
placeholder="blur"
blurDataURL={product.blurDataUrl}
priority={product.featured}
/>
</div>
);
}
Fonts Automatiques
// app/layout.tsx
import { Inter, JetBrains_Mono } from 'next/font/google';
const inter = Inter({
subsets: ['latin'],
display: 'swap',
variable: '--font-inter',
});
const jetbrainsMono = JetBrains_Mono({
subsets: ['latin'],
display: 'swap',
variable: '--font-mono',
});
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="fr" className={`${inter.variable} ${jetbrainsMono.variable}`}>
<body className="font-sans">
{children}
</body>
</html>
);
}
Metadata API
Métadonnées Statiques et Dynamiques
// app/blog/[slug]/page.tsx
import type { Metadata, ResolvingMetadata } from 'next';
type Props = {
params: { slug: string };
};
export async function generateMetadata(
{ params }: Props,
parent: ResolvingMetadata
): Promise<Metadata> {
const post = await getPost(params.slug);
// Hériter des métadonnées parent
const previousImages = (await parent).openGraph?.images || [];
return {
title: post.title,
description: post.excerpt,
authors: [{ name: post.author.name }],
openGraph: {
title: post.title,
description: post.excerpt,
type: 'article',
publishedTime: post.publishedAt,
authors: [post.author.name],
images: [
{
url: post.coverImage,
width: 1200,
height: 630,
alt: post.title,
},
...previousImages,
],
},
twitter: {
card: 'summary_large_image',
title: post.title,
description: post.excerpt,
images: [post.coverImage],
},
};
}
export default async function BlogPost({ params }: Props) {
const post = await getPost(params.slug);
// ...
}
Migration vers Next.js 15
Étapes de Migration
# 1. Mise à jour des dépendances
npm install next@15 react@19 react-dom@19
# 2. Mise à jour des types
npm install -D @types/react@19 @types/react-dom@19
# 3. Vérification avec codemods
npx @next/codemod@latest upgrade
Changements Breaking
// ❌ Avant : Cache par défaut
const data = await fetch('/api/data');
// ✅ Après : Cache explicite
const data = await fetch('/api/data', {
next: { revalidate: 60 }
});
// Ou forcer le comportement dynamique
export const dynamic = 'force-dynamic';
Bonnes Pratiques Next.js 15
1. Utilisez les Server Components par Défaut
// ✅ Server Component - accès direct aux données
async function ProductList() {
const products = await db.product.findMany();
return <ul>{products.map(p => <li key={p.id}>{p.name}</li>)}</ul>;
}
// Client Component uniquement pour l'interactivité
'use client';
function AddToCartButton({ productId }: { productId: string }) {
// ...
}
2. Server Actions pour les Mutations
// ✅ Server Action - pas d'API Route nécessaire
'use server';
export async function addToCart(productId: string) {
// Directement dans le serveur
}
3. Cache Explicite
// ✅ Toujours explicite sur le comportement de cache
export const revalidate = 3600; // Revalidation toutes les heures
Conclusion
Next.js 15 représente une version majeure qui consolide les innovations des versions précédentes tout en apportant des améliorations significatives :
Points clés à retenir :
- Turbopack stable : Performances de développement exceptionnelles
- Server Actions matures : Mutations simplifiées sans API séparée
- Cache explicite : Comportement prévisible et contrôlable
- App Router optimisé : Parallel routes, intercepting routes, streaming
- React 19 intégré : Toutes les nouvelles fonctionnalités disponibles
La combinaison de ces fonctionnalités fait de Next.js 15 le framework idéal pour construire des applications React modernes, performantes et maintenables.
Conseil pratique : Commencez par migrer vos API Routes vers des Server Actions pour les mutations simples, puis adoptez progressivement les patterns avancés de l'App Router.
Besoin d'aide pour développer votre projet avec Next.js 15 ? Contactez Raicode pour discuter de vos besoins.
Prêt à lancer votre projet ?
Transformez vos idées en réalité avec un développeur passionné par la performance et le SEO. Discutons de votre projet dès aujourd'hui.


