Rate limiting et throttling pour APIs
Introduction Le rate limiting et le throttling sont des mécanismes essentiels pour protéger vos APIs…
Next.js 14+ révolutionne le développement React avec l’App Router, les Server Components, et des performances exceptionnelles. Découvrez comment créer des applications ultra-rapides avec les stratégies de rendu modernes.
# Créer un nouveau projet
npx create-next-app@latest mon-app-nextjs
# Options recommandées
✔ Would you like to use TypeScript? … Yes
✔ Would you like to use ESLint? … Yes
✔ Would you like to use Tailwind CSS? … Yes
✔ Would you like to use src/ directory? … Yes
✔ Would you like to use App Router? … Yes
✔ Would you like to customize the default import alias (@/)? … Yes
# Naviguer et lancer
cd mon-app-nextjs
npm run dev
mon-app-nextjs/
├── src/
│ ├── app/
│ │ ├── layout.tsx # Layout racine
│ │ ├── page.tsx # Page d'accueil
│ │ ├── globals.css # Styles globaux
│ │ ├── api/ # API Routes
│ │ │ └── users/
│ │ │ └── route.ts
│ │ ├── blog/ # Route /blog
│ │ │ ├── page.tsx
│ │ │ ├── [slug]/ # Route dynamique /blog/[slug]
│ │ │ │ └── page.tsx
│ │ │ └── loading.tsx # Loading UI
│ │ ├── dashboard/
│ │ │ ├── layout.tsx # Layout imbriqué
│ │ │ ├── page.tsx
│ │ │ └── error.tsx # Error UI
│ │ └── not-found.tsx # 404 page
│ ├── components/
│ │ ├── Header.tsx
│ │ └── Footer.tsx
│ └── lib/
│ └── api.ts
├── public/
│ ├── images/
│ └── favicon.ico
├── next.config.js
├── tsconfig.json
└── package.json
// src/app/page.tsx - Route: /
export default function HomePage() {
return (
# Bienvenue
Page d'accueil
);
}
// src/app/about/page.tsx - Route: /about
export default function AboutPage() {
return (
# À propos
);
}
// src/app/blog/page.tsx - Route: /blog
export default function BlogPage() {
return (
# Blog
);
}
// src/app/blog/[slug]/page.tsx - Route: /blog/mon-article
interface PageProps {
params: {
slug: string;
};
searchParams: {
[key: string]: string | string[] | undefined;
};
}
export default function BlogPostPage({ params, searchParams }: PageProps) {
return (
# Article: {params.slug}
Query params: {JSON.stringify(searchParams)}
);
}
// src/app/shop/[category]/[product]/page.tsx
// Route: /shop/electronics/laptop
interface ProductPageProps {
params: {
category: string;
product: string;
};
}
export default function ProductPage({ params }: ProductPageProps) {
return (
# Catégorie: {params.category}
## Produit: {params.product}
);
}
// src/app/docs/[...slug]/page.tsx
// Matche: /docs/a, /docs/a/b, /docs/a/b/c
interface DocsPageProps {
params: {
slug: string[];
};
}
export default function DocsPage({ params }: DocsPageProps) {
return (
# Documentation
Path: {params.slug.join('/')}
);
}
// src/app/shop/[[...slug]]/page.tsx
// Matche: /shop, /shop/a, /shop/a/b (optional catch-all)
src/app/
├── (marketing)/ # Groupe sans effet sur l'URL
│ ├── layout.tsx # Layout partagé
│ ├── page.tsx # Route: /
│ ├── about/
│ │ └── page.tsx # Route: /about
│ └── contact/
│ └── page.tsx # Route: /contact
├── (shop)/
│ ├── layout.tsx
│ ├── products/
│ │ └── page.tsx # Route: /products
│ └── cart/
│ └── page.tsx # Route: /cart
└── dashboard/
└── page.tsx # Route: /dashboard
// src/app/layout.tsx
import type { Metadata } from 'next';
import { Inter } from 'next/font/google';
import './globals.css';
const inter = Inter({ subsets: ['latin'] });
export const metadata: Metadata = {
title: {
default: 'Mon App',
template: '%s | Mon App'
},
description: 'Application Next.js moderne',
keywords: ['Next.js', 'React', 'TypeScript']
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
{children}
);
}
Layouts imbriqués
// src/app/dashboard/layout.tsx
export default function DashboardLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
{children}
);
}
// src/app/dashboard/page.tsx
export default function DashboardPage() {
return # Dashboard;
}
// Render: RootLayout > DashboardLayout > DashboardPage
Templates (réinitialisation du state)
// src/app/dashboard/template.tsx
// Contrairement au layout, le template crée une nouvelle instance à chaque navigation
export default function DashboardTemplate({
children,
}: {
children: React.ReactNode;
}) {
return (
{children}
);
}
4. Server Components vs Client Components
Server Components (par défaut)
// src/app/blog/page.tsx
// Server Component par défaut - pas de 'use client'
async function getPosts() {
const res = await fetch('https://api.example.com/posts', {
next: { revalidate: 60 } // ISR: revalide toutes les 60 secondes
});
return res.json();
}
export default async function BlogPage() {
const posts = await getPosts();
return (
# Blog
{posts.map((post: any) => (
## {post.title}
{post.excerpt}
))}
);
}
Client Components
// src/components/Counter.tsx
'use client'; // Directive obligatoire pour les Client Components
import { useState } from 'react';
export default function Counter() {
const [count, setCount] = useState(0);
return (
Count: {count}
);
}
Composition Server + Client
// src/app/blog/[slug]/page.tsx
// Server Component
import CommentForm from '@/components/CommentForm'; // Client Component
import CommentList from '@/components/CommentList'; // Server Component
async function getPost(slug: string) {
const res = await fetch(https://api.example.com/posts/${slug});
return res.json();
}
export default async function BlogPostPage({
params
}: {
params: { slug: string }
}) {
const post = await getPost(params.slug);
return (
# {post.title}
html: post.content }} />
{/ Server Component imbriqué /}
{/ Client Component pour l'interactivité /}
);
}
Quand utiliser Server vs Client Components
// ✅ Server Components pour:
// - Fetch de données
// - Accès direct à la base de données
// - Secrets backend (API keys)
// - Dépendances lourdes qui restent sur le serveur
// - SEO et performance
// ✅ Client Components pour:
// - Interactivité (onClick, onChange...)
// - State et effects (useState, useEffect)
// - Browser APIs (localStorage, window...)
// - Event listeners
// - Custom hooks utilisant state/effects
5. Stratégies de rendu
Static Site Generation (SSG)
// src/app/blog/[slug]/page.tsx
// Générer les pages statiques au build time
export async function generateStaticParams() {
const posts = await fetch('https://api.example.com/posts').then(res => res.json());
return posts.map((post: any) => ({
slug: post.slug,
}));
}
// Cette fonction s'exécute au build time
async function getPost(slug: string) {
const res = await fetch(https://api.example.com/posts/${slug}, {
next: { revalidate: false } // Cache permanent
});
return res.json();
}
export default async function Page({ params }: { params: { slug: string } }) {
const post = await getPost(params.slug);
return (
# {post.title}
{post.content}
);
}
// Metadata dynamique pour SEO
export async function generateMetadata({
params
}: {
params: { slug: string }
}) {
const post = await getPost(params.slug);
return {
title: post.title,
description: post.excerpt,
};
}
Incremental Static Regeneration (ISR)
// src/app/products/page.tsx
async function getProducts() {
const res = await fetch('https://api.example.com/products', {
next: {
revalidate: 3600 // Revalide toutes les heures
}
});
return res.json();
}
export default async function ProductsPage() {
const products = await getProducts();
return (
# Produits
{products.map((product: any) => (
## {product.name}
{product.price}€
))}
);
}
Server-Side Rendering (SSR)
// src/app/dashboard/page.tsx
// Force le dynamic rendering (SSR)
export const dynamic = 'force-dynamic';
// Ou export const revalidate = 0;
async function getUserData() {
const res = await fetch('https://api.example.com/user', {
cache: 'no-store' // Pas de cache
});
return res.json();
}
export default async function DashboardPage() {
const user = await getUserData();
return (
# Bienvenue {user.name}
Dernière connexion: {new Date().toLocaleString()}
);
}
Comparaison des stratégies
// SSG - Généré au build time
fetch('...', { next: { revalidate: false } })
// Avantages: Ultra rapide, CDN cacheable
// Inconvénients: Données potentiellement obsolètes
// ISR - Régénération incrémentale
fetch('...', { next: { revalidate: 60 } })
// Avantages: Équilibre performance/fraîcheur
// Inconvénients: Première requête peut être lente
// SSR - Rendu à chaque requête
fetch('...', { cache: 'no-store' })
export const dynamic = 'force-dynamic';
// Avantages: Données toujours fraîches
// Inconvénients: Plus lent, charge serveur
// On-demand Revalidation - Revalider manuellement
// Via API Route ou Server Action
revalidatePath('/blog');
revalidateTag('posts');
6. Data Fetching moderne
Fetch avec cache
// Cache persistant
const res = await fetch('https://api.example.com/data');
// Équivalent à: { cache: 'force-cache' }
// Pas de cache
const res = await fetch('https://api.example.com/data', {
cache: 'no-store'
});
// Revalidation périodique
const res = await fetch('https://api.example.com/data', {
next: { revalidate: 3600 }
});
// Cache avec tags (pour revalidation on-demand)
const res = await fetch('https://api.example.com/data', {
next: { tags: ['posts'] }
});
Requêtes parallèles
// src/app/dashboard/page.tsx
async function getUser() {
const res = await fetch('https://api.example.com/user');
return res.json();
}
async function getPosts() {
const res = await fetch('https://api.example.com/posts');
return res.json();
}
async function getStats() {
const res = await fetch('https://api.example.com/stats');
return res.json();
}
export default async function DashboardPage() {
// ✅ Requêtes parallèles
const [user, posts, stats] = await Promise.all([
getUser(),
getPosts(),
getStats()
]);
return (
# {user.name}
{posts.length} posts
{stats.views} vues
);
}
Streaming avec Suspense
// src/app/dashboard/page.tsx
import { Suspense } from 'react';
async function UserProfile() {
const user = await getUser(); // Fetch lent
return {user.name};
}
async function PostList() {
const posts = await getPosts(); // Fetch rapide
return {posts.map(p => - {p.title}
)}
;
}
export default function DashboardPage() {
return (
# Dashboard
{/ Affiche immédiatement le fallback, puis le composant */}
Chargement du profil... }>
Chargement des posts...
}>
);
}
7. Server Actions
Action simple
// src/app/actions.ts
'use server';
import { revalidatePath } from 'next/cache';
export async function createPost(formData: FormData) {
const title = formData.get('title') as string;
const content = formData.get('content') as string;
// Validation
if (!title || !content) {
return { error: 'Titre et contenu requis' };
}
// Sauvegarder en base de données
const res = await fetch('https://api.example.com/posts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title, content })
});
if (!res.ok) {
return { error: 'Erreur lors de la création' };
}
// Revalider le cache
revalidatePath('/blog');
return { success: true };
}
Formulaire avec Server Action
// src/app/blog/new/page.tsx
import { createPost } from '@/app/actions';
export default function NewPostPage() {
return (
);
}
Server Action avec état côté client
// src/components/CreatePostForm.tsx
'use client';
import { useFormState, useFormStatus } from 'react-dom';
import { createPost } from '@/app/actions';
function SubmitButton() {
const { pending } = useFormStatus();
return (
);
}
export default function CreatePostForm() {
const [state, formAction] = useFormState(createPost, { error: null });
return (
);
}
Server Action avec optimistic UI
'use client';
import { useOptimistic } from 'react';
import { addComment } from '@/app/actions';
interface Comment {
id: string;
text: string;
}
export default function CommentList({ comments }: { comments: Comment[] }) {
const [optimisticComments, addOptimisticComment] = useOptimistic(
comments,
(state, newComment: string) => [
...state,
{ id: 'temp-' + Date.now(), text: newComment }
]
);
async function handleSubmit(formData: FormData) {
const text = formData.get('text') as string;
// Mise à jour optimiste
addOptimisticComment(text);
// Server Action
await addComment(formData);
}
return (
{optimisticComments.map(comment => (
- {comment.text}
))}
);
}
8. API Routes avec Route Handlers
GET Handler
// src/app/api/posts/route.ts
import { NextRequest, NextResponse } from 'next/server';
export async function GET(request: NextRequest) {
const searchParams = request.nextUrl.searchParams;
const page = searchParams.get('page') || '1';
const posts = await fetch(https://api.example.com/posts?page=${page})
.then(res => res.json());
return NextResponse.json(posts);
}
POST Handler
// src/app/api/posts/route.ts
export async function POST(request: NextRequest) {
const body = await request.json();
// Validation
if (!body.title || !body.content) {
return NextResponse.json(
{ error: 'Titre et contenu requis' },
{ status: 400 }
);
}
// Créer le post
const post = await fetch('https://api.example.com/posts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
}).then(res => res.json());
return NextResponse.json(post, { status: 201 });
}
Route dynamique
// src/app/api/posts/[id]/route.ts
export async function GET(
request: NextRequest,
{ params }: { params: { id: string } }
) {
const post = await fetch(https://api.example.com/posts/${params.id})
.then(res => res.json());
if (!post) {
return NextResponse.json(
{ error: 'Post introuvable' },
{ status: 404 }
);
}
return NextResponse.json(post);
}
export async function DELETE(
request: NextRequest,
{ params }: { params: { id: string } }
) {
await fetch(https://api.example.com/posts/${params.id}, {
method: 'DELETE'
});
return NextResponse.json({ success: true });
}
9. Metadata et SEO
Metadata statique
// src/app/page.tsx
import type { Metadata } from 'next';
export const metadata: Metadata = {
title: 'Accueil',
description: 'Page d'accueil de mon site',
keywords: ['next.js', 'react', 'seo'],
authors: [{ name: 'Votre Nom' }],
openGraph: {
title: 'Mon Site',
description: 'Description pour les réseaux sociaux',
images: ['/og-image.jpg'],
},
twitter: {
card: 'summarylargeimage',
title: 'Mon Site',
description: 'Description pour Twitter',
images: ['/twitter-image.jpg'],
},
};
Metadata dynamique
// src/app/blog/[slug]/page.tsx
export async function generateMetadata({
params
}: {
params: { slug: string }
}): Promise {
const post = await fetch(https://api.example.com/posts/${params.slug})
.then(res => res.json());
return {
title: post.title,
description: post.excerpt,
openGraph: {
title: post.title,
description: post.excerpt,
images: [post.image],
},
};
}
10. Tests Next.js
// tests/page.test.tsx
import { render, screen } from '@testing-library/react';
import Home from '@/app/page';
describe('Home', () => {
it('renders homepage', () => {
render( );
const heading = screen.getByRole('heading', { level: 1 });
expect(heading).toBeInTheDocument();
});
});
// tests__/api.test.ts
import { GET } from '@/app/api/posts/route';
import { NextRequest } from 'next/server';
describe('/api/posts', () => {
it('returns posts', async () => {
const request = new NextRequest('http://localhost:3000/api/posts');
const response = await GET(request);
const data = await response.json();
expect(Array.isArray(data)).toBe(true);
});
});
Performance Benchmarks
Temps de chargement (LCP)
Stratégie
First Load
Subsequent
SEO
SSG
0.5s
0.2s
Excellent
ISR
0.6s
0.3s
Excellent
SSR
1.2s
0.8s
Excellent
CSR (React)
2.5s
0.5s
Moyen
Meilleures pratiques 2025
- Server Components par défaut, Client Components seulement si nécessaire
- ISR pour le meilleur équilibre performance/fraîcheur
- Streaming avec Suspense pour améliorer le Time to First Byte
- Server Actions pour les mutations sans API Routes
- generateStaticParams pour le SSG de routes dynamiques
- Metadata API pour le SEO optimal
Conclusion
Next.js 14+ avec l’App Router offre une expérience de développement moderne et des performances exceptionnelles grâce aux Server Components et aux stratégies de rendu flexibles.
Points clés
- App Router pour un routing intuitif
- Server Components pour de meilleures performances
- SSG/ISR/SSR selon vos besoins
- Server Actions pour simplifier les mutations
- Metadata API pour un SEO optimal
Ressources
- Next.js Documentation
- Next.js Learn
- Vercel Templates
Cet article est vivant — corrections, contre-arguments et retours de production sont les bienvenus. Trois canaux, choisissez celui qui vous convient.