💻 Développement

dev-nextjs-guide

Développement d'applications Next.js avec App Router, Server Components, SSR, ISR, API routes et middleware.

⚡ Installation & lancement en 1 commande

Copiez-collez dans votre terminal : le skill s'installe dans ~/.claude/skills et Claude Code se lance directement dessus.

macOS / Linux
curl -fsSL https://raw.githubusercontent.com/khalilbenaz/claude-skills-collection/main/install.sh | sh -s -- dev-nextjs-guide --launch
Windows (PowerShell)
iex "& { $(iwr -useb https://raw.githubusercontent.com/khalilbenaz/claude-skills-collection/main/install.ps1) } dev-nextjs-guide -Launch"

🚀 Déjà installé ?

claude "/dev-nextjs-guide"

Ou tapez /dev-nextjs-guide dans une session Claude Code, ou décrivez simplement votre besoin — le skill se déclenche automatiquement via le skill-router.

🔑 Déclencheurs automatiques

Le skill s'active automatiquement quand votre demande contient :

Next.jsNextJSApp RouterServer ComponentsSSRgetServerSideProps

📦 Installation manuelle

git clone https://github.com/khalilbenaz/claude-skills-collection.git cp -r claude-skills-collection/skills/dev-nextjs-guide ~/.claude/skills/

Payload du plugin : skills/dev-nextjs-guide · source éditable : dev-skills/nextjs-guide

📖 Manuel

Guide Next.js (App Router — 2026)

1. Critères de décision stratégique

Choix de la stratégie de rendu par page :

BesoinStratégieConfig
Données dynamiques par requêteSSRexport const dynamic = 'force-dynamic'
Données rarement modifiéesISRexport const revalidate = 60
Contenu totalement statiqueSSGgenerateStaticParams + cache par défaut
Interactivité pure, auth clientCSR"use client" + SWR/React Query
Latence critique, edge-compatibleEdge SSRexport const runtime = 'edge'

Quand utiliser "use client" : uniquement pour useState, useEffect, event handlers DOM, hooks navigateur (window, localStorage). Tout le reste = Server Component.


2. Initialisation du projet

# Next.js 15 avec TypeScript, Tailwind, App Router, ESLint
npx create-next-app@latest my-app \
  --typescript --tailwind --app --eslint --src-dir --import-alias "@/*"

cd my-app && npm run dev

Structure app/ recommandée :

app/
  (marketing)/          # groupe de routes sans segment URL
    page.tsx
    layout.tsx
  (dashboard)/
    layout.tsx           # layout partagé dashboard
    analytics/page.tsx
    settings/page.tsx
  api/
    users/route.ts       # Route Handler REST
  globals.css
  layout.tsx             # Root layout (obligatoire)
  not-found.tsx

3. Server Components — data fetching

// app/users/page.tsx — Server Component par défaut
async function getUsers() {
  const res = await fetch('https://api.example.com/users', {
    next: { revalidate: 60 }, // ISR : re-valide toutes les 60s
  });
  if (!res.ok) throw new Error('Fetch failed');
  return res.json();
}

export default async function UsersPage() {
  const users = await getUsers(); // pas de useEffect, direct
  return <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
}

Parallel fetching (évite les waterfalls) :

export default async function DashboardPage() {
  const [users, stats] = await Promise.all([
    getUsers(),
    getStats(),
  ]);
  return <Dashboard users={users} stats={stats} />;
}

4. Server Actions — mutations de données

// app/actions/user.ts
'use server';
import { z } from 'zod';
import { revalidatePath } from 'next/cache';

const CreateUserSchema = z.object({
  name: z.string().min(2),
  email: z.string().email(),
});

export async function createUser(formData: FormData) {
  const parsed = CreateUserSchema.safeParse({
    name: formData.get('name'),
    email: formData.get('email'),
  });
  if (!parsed.success) return { error: parsed.error.flatten() };

  await db.user.create({ data: parsed.data });
  revalidatePath('/users'); // invalide le cache de la page
}
// Utilisation dans un form
<form action={createUser}>
  <input name="name" required />
  <input name="email" type="email" required />
  <button type="submit">Créer</button>
</form>

5. Route Handlers (API REST)

// app/api/users/[id]/route.ts
import { NextRequest, NextResponse } from 'next/server';

export async function GET(
  request: NextRequest,
  { params }: { params: Promise<{ id: string }> }
) {
  const { id } = await params; // Next 15 : params est une Promise
  const user = await db.user.findUnique({ where: { id } });
  if (!user) return NextResponse.json({ error: 'Not found' }, { status: 404 });
  return NextResponse.json(user);
}

export async function DELETE(
  request: NextRequest,
  { params }: { params: Promise<{ id: string }> }
) {
  const { id } = await params;
  await db.user.delete({ where: { id } });
  return new NextResponse(null, { status: 204 });
}

6. Middleware — auth, redirections, headers

// middleware.ts (racine du projet)
import { NextRequest, NextResponse } from 'next/server';

export function middleware(request: NextRequest) {
  const token = request.cookies.get('session')?.value;
  const isProtected = request.nextUrl.pathname.startsWith('/dashboard');

  if (isProtected && !token) {
    return NextResponse.redirect(new URL('/login', request.url));
  }

  const response = NextResponse.next();
  response.headers.set('X-Frame-Options', 'DENY');
  response.headers.set('X-Content-Type-Options', 'nosniff');
  return response;
}

export const config = {
  matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
};

7. Streaming et Suspense

// app/dashboard/page.tsx
import { Suspense } from 'react';
import { UsersSkeleton } from '@/components/skeletons';

export default function DashboardPage() {
  return (
    <div>
      <h1>Dashboard</h1>
      <Suspense fallback={<UsersSkeleton />}>
        <SlowDataComponent />  {/* streamé indépendamment */}
      </Suspense>
    </div>
  );
}

8. Optimisation images et polices

import Image from 'next/image';
import { Inter } from 'next/font/google';

const inter = Inter({ subsets: ['latin'], display: 'swap' });

// Image optimisée avec lazy loading automatique
<Image
  src="/hero.webp"
  alt="Hero"
  width={1200}
  height={630}
  priority // uniquement pour LCP (above the fold)
/>

Lazy loading composant client lourd :

import dynamic from 'next/dynamic';
const HeavyChart = dynamic(() => import('@/components/HeavyChart'), {
  ssr: false, // si besoin de window
  loading: () => <p>Chargement...</p>,
});

9. Variables d'environnement

# .env.local
DATABASE_URL="postgres://..."          # serveur uniquement
NEXT_PUBLIC_API_URL="https://api..."   # exposé côté client (préfixe obligatoire)

10. Garde-fous et anti-patterns

Waterfalls de fetch — ne jamais faire await en séquence quand les requêtes sont indépendantes. Utiliser Promise.all.

"use client" trop haut — ne pas marquer un layout ou une page entière comme client. Pousser "use client" au composant feuille qui a besoin d'interactivité.

Données sensibles dans les composants client — ne jamais passer process.env.SECRET_KEY ou tokens JWT à un composant client. Les garder dans les Server Actions ou Route Handlers.

Oublier revalidatePath/revalidateTag après une mutation — la page affichera des données périmées après une Server Action.

params non awaité (Next 15)params et searchParams sont des Promises dans Next.js 15. Toujours les await.

export const dynamic = 'force-dynamic' sur tout — désactive le cache partout, détruit les performances. Utiliser uniquement quand les données changent à chaque requête.

Fetch sans gestion d'erreur — un fetch échoué dans un Server Component non protégé par error.tsx fait planter toute la page.


11. Déploiement

# Build de production
npm run build && npm start

# Standalone (Docker)
# next.config.ts
output: 'standalone'

# Dockerfile minimal
FROM node:22-alpine AS runner
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
CMD ["node", "server.js"]

Vercel : zéro config — vercel deploy --prod. ISR, Edge Functions et Image Optimization activés automatiquement.

Self-hosted : vérifier que HOSTNAME=0.0.0.0 est défini pour le serveur standalone (sinon écoute sur localhost seulement).