RAICODE
ProcessusProjetsBlogOffresClientsContact
development

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.

Mustapha Hamadi
Développeur Full-Stack
5 décembre 2025
12 min read
Next.js 15 : App Router, Server Actions et les Nouvelles Fonctionnalités à Connaître
#Next.js#React#Full-Stack
Partager :

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.

Partager :

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.

Demander un devis
Voir mes réalisations
Réponse < 48h
15+ projets livrés
100% satisfaction client

Table des matières

Articles similaires

De 0 à Production en 4 Heures : Speed-Run d'un Site E-commerce
development

De 0 à Production en 4 Heures : Speed-Run d'un Site E-commerce

8 décembre 2025
15 min read
Intégrer l'IA dans Votre Application Web : Guide Pratique avec l'API OpenAI
development

Intégrer l'IA dans Votre Application Web : Guide Pratique avec l'API OpenAI

7 décembre 2025
15 min read
Sécuriser une Application Next.js : Authentification et Bonnes Pratiques
development

Sécuriser une Application Next.js : Authentification et Bonnes Pratiques

7 décembre 2025
15 min read
RAICODE

Développeur Full-Stack spécialisé en Next.js & React.
Je crée des applications web performantes et sur mesure.

SERVICES

  • Sites Vitrines
  • Applications SaaS
  • E-commerce
  • API & Backend

NAVIGATION

  • Processus
  • Projets
  • Blog
  • Tarifs
  • Contact

LÉGAL

  • Mentions légales
  • Confidentialité
  • CGU
  • CGV

© 2025 Raicode. Tous droits réservés.

Créé parRaicode.
↑ Retour en haut