Changer le thème

Next.js 15 en pratique : comment j'ai construit un blog de niveau production en un week-end

Easton editorial illustration: API gateway workstation

Introduction

Next.js 15 est sorti depuis peu. J’ai lu la doc plusieurs fois et regardé des tutos, mais tout restait théorique — Server Actions, App Router : je comprenais les concepts, pas leur usage dans un vrai projet. Ce week-end, j’ai arrêté les vidéos et construit un blog full-stack. Frontend, backend, base de données, déploiement : deux jours plus tard, le blog tournait sur Vercel avec un score Lighthouse de 96.

Cet article raconte le parcours — pas un tuto copier-coller, mais le pourquoi des choix et les pièges. Stack : Next.js 15 + Server Actions + Prisma + PostgreSQL, du code prêt pour la production.

Pourquoi Next.js 15 ?

Le choix de la stack m’a fait hésiter longtemps.

À la sortie de Next.js 15, certains dans la communauté disaient : « Encore une mise à jour, j’en peux plus. » Honnêtement, j’étais un peu réticent — la 14 n’était pas encore totalement maîtrisée. En creusant les nouveautés, j’ai compris que cette montée de version valait vraiment le coup.

Server Actions : fini la corvée des API Routes

En full-stack, ce qui m’agace le plus, c’était les API Routes. Créer un api/posts/route.ts, définir POST, parser le body, renvoyer la réponse… toujours le même rituel.

Les Server Actions changent la donne. Vous ajoutez 'use server' devant la fonction et vous l’appelez depuis le composant. Au début, je me disais : « On met du backend dans le frontend ? » Un essai — et c’est adopté.

Exemple : avant, pour créer un article :

// Ancienne méthode : API Route obligatoire
// app/api/posts/route.ts
export async function POST(request: Request) {
  const body = await request.json()
  // plein de logique...
}
// Côté client : fetch
const response = await fetch('/api/posts', {
  method: 'POST',
  body: JSON.stringify(data)
})

Maintenant :

// app/actions/post-actions.ts
'use server'
export async function createPost(formData: FormData) {
  const title = formData.get('title')
  const content = formData.get('content')
  // accès direct à la base
  return await prisma.post.create({
    data: { title, content }
  })
}

Dans le composant, c’est comme appeler une fonction locale. Image parlante : avant, vous alliez chercher la commande au restaurant (API Routes) ; maintenant, elle est livrée chez vous (Server Actions).

Turbopack : le dev va bien plus vite

Next.js 15 a stabilisé Turbopack. Officiellement : +76,7 % au démarrage du serveur local, +96,3 % sur les mises à jour de code. J’ai cru à du marketing — en pratique, les chiffres tiennent la route.

76,7 %
Gain vitesse de démarrage serveur
Données de tests officielles
96,3 %
Gain vitesse de mise à jour code
Hot reload quasi instantané
2 s
Temps de démarrage du projet
~30 composants : de 7-8 s (Webpack) à moins de 2 s

Mon projet compte une trentaine de composants. Avec Webpack : 7-8 s au lancement ; avec Turbopack : moins de 2 s. Une modification de code, le hot reload est quasi immédiat. Le confort au quotidien n’est pas négligeable.

Pourquoi cette stack pour un blog ?

Trois raisons principales :

SSR/SSG natifs — Le SEO compte avant tout. Le rendu serveur et la génération statique de Next.js sont faits pour ça. Google reçoit du HTML complet, bien plus efficace qu’un React 100 % client.

Typage Prisma — Avec Mongoose sur MongoDB, les types se maintenaient à la main ; les bugs arrivaient vite. Prisma génère les types TypeScript depuis le schema : IntelliSense complet, peu d’erreurs.

Server Actions — Plus besoin d’API Routes : au moins 30 % de code en moins. En mode classique, j’aurais ajouté plusieurs fichiers route.ts.

Choix de stack et architecture

Une fois le pourquoi clair, place au comment.

Ma stack complète

  • Frontend : Next.js 15 + TypeScript + Tailwind CSS
  • Backend : Server Actions Next.js + NextAuth.js
  • Base de données : PostgreSQL + Prisma ORM
  • Déploiement : Vercel

Pourquoi pas MongoDB ? J’y ai pensé — c’est flexible. Mais Prisma supporte mieux PostgreSQL, et pour articles, utilisateurs et commentaires, le relationnel colle mieux.

Structure des dossiers

my-blog/
├── app/
│   ├── (auth)/          # Pages d'authentification
│   ├── blog/            # Pages du blog
│   │   └── [slug]/      # Routes dynamiques
│   ├── dashboard/       # Tableau de bord
│   ├── actions/         # Toutes les Server Actions
│   └── api/auth/        # Config NextAuth
├── components/          # Composants réutilisables
├── lib/
│   ├── prisma.ts        # Singleton Prisma (crucial !)
│   └── utils.ts         # Utilitaires
├── prisma/
│   └── schema.prisma    # Schema base de données
└── public/              # Assets statiques

J’ai itéré plusieurs fois. Au début, les Server Actions étaient éparpillées dans les pages ; la maintenance devenait ingérable. Un dossier actions dédié a tout simplifié.

Design du schema base de données

Gros piège ici. Ma première version était trop riche : tags, catégories, stats de lecture… À mi-parcours, j’ai tout simplifié — une demi-journée perdue.

Schema final :

// prisma/schema.prisma
generator client {
  provider = "prisma-client-js"
}
datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}
model User {
  id        String   @id @default(cuid())
  email     String   @unique
  name      String?
  image     String?
  posts     Post[]
  createdAt DateTime @default(now())
}
model Post {
  id          String   @id @default(cuid())
  title       String
  slug        String   @unique
  content     String   @db.Text
  published   Boolean  @default(false)
  authorId    String
  author      User     @relation(fields: [authorId], references: [id])
  createdAt   DateTime @default(now())
  updatedAt   DateTime @updatedAt
  @@index([slug])
  @@index([authorId])
}

Les deux @@index sont clés pour la perf : requêtes par slug sur la liste, filtre par authorId sur le profil — les index accélèrent nettement les requêtes.

Implémentation des fonctionnalités clés

Architecture posée — place au code.

3.1 Installation et initialisation

Créer un projet Next.js 15 :

npx create-next-app@latest my-blog
cd my-blog
npm install prisma @prisma/client zod next-auth

Puis Prisma :

npx prisma init

Cela crée prisma/schema.prisma et .env. Connexion PostgreSQL :

DATABASE_URL="postgresql://username:password@localhost:5432/myblog?schema=public"

En local, PostgreSQL via Docker :

docker run --name blog-postgres -e POSTGRES_PASSWORD=mypassword -p 5432:5432 -d postgres

3.2 Intégration Prisma

Après le schema, migration :

npx prisma migrate dev --name init

Piège classique : le singleton Prisma Client. Au premier déploiement Vercel, je l’avais oublié — après quelques heures, « Too many connections ». J’ai cru à une attaque DDoS.

En dev, le hot reload recrée des instances Prisma et sature le pool. Solution :

// lib/prisma.ts
import { PrismaClient } from '@prisma/client'
const globalForPrisma = globalThis as unknown as {
  prisma: PrismaClient | undefined
}
export const prisma = globalForPrisma.prisma ?? new PrismaClient()
if (process.env.NODE_ENV !== 'production') {
  globalForPrisma.prisma = prisma
}

Un peu tordu à lire, mais une seule instance en dev, production inchangée. Bonne pratique officielle Prisma.

3.3 Server Actions en pratique

Créer un article, fonction centrale :

// app/actions/post-actions.ts
'use server'
import { prisma } from '@/lib/prisma'
import { revalidatePath } from 'next/cache'
import { z } from 'zod'
// Validation Zod pour la sécurité des types
const PostSchema = z.object({
  title: z.string().min(1, '标题不能为空').max(100),
  content: z.string().min(10, '内容至少10个字'),
  slug: z.string().regex(/^[a-z0-9-]+$/, 'slug只能包含小写字母、数字和横杠')
})
export async function createPost(formData: FormData) {
  const validatedFields = PostSchema.safeParse({
    title: formData.get('title'),
    content: formData.get('content'),
    slug: formData.get('slug')
  })
  if (!validatedFields.success) {
    return { error: '数据验证失败' }
  }
  try {
    const post = await prisma.post.create({
      data: {
        ...validatedFields.data,
        authorId: 'user-id-here' // en prod : depuis la session
      }
    })
    revalidatePath('/blog')
    return { success: true, post }
  } catch (error) {
    return { error: '创建文章失败' }
  }
}

Dans le composant :

// app/dashboard/new-post/page.tsx
import { createPost } from '@/app/actions/post-actions'
export default function NewPost() {
  return (
    <form action={createPost}>
      <input name="title" placeholder="标题" />
      <textarea name="content" placeholder="内容" />
      <input name="slug" placeholder="URL slug" />
      <button type="submit">发布</button>
    </form>
  )
}

Pas de useState, pas de fetch, pas de onSubmit. Le formulaire appelle la Server Action ; Next.js gère sérialisation, réseau et erreurs.

Au début, je confondais Server Actions et API Routes : livraison à domicile vs aller au restaurant. Dans la majorité des cas, les Server Actions suffisent.

3.4 Authentification

J’ai utilisé NextAuth.js :

// app/api/auth/[...nextauth]/route.ts
import NextAuth from 'next-auth'
import GitHubProvider from 'next-auth/providers/github'
import { PrismaAdapter } from '@auth/prisma-adapter'
import { prisma } from '@/lib/prisma'
export const authOptions = {
  adapter: PrismaAdapter(prisma),
  providers: [
    GitHubProvider({
      clientId: process.env.GITHUB_ID!,
      clientSecret: process.env.GITHUB_SECRET!
    })
  ]
}
const handler = NextAuth(authOptions)
export { handler as GET, handler as POST }

OAuth GitHub : créer une app dans GitHub Developer Settings, renseigner Client ID et Secret dans .env. Dix minutes, bien moins pénible qu’un JWT maison.

3.5 Stratégie SSR/SSG

Au début, tout en SSR : chaque ouverture de la liste interrogeait la base — inutile. J’ai adopté une approche hybride.

Liste du blog — SSG + ISR :

// app/blog/page.tsx
import { prisma } from '@/lib/prisma'
// Régénération toutes les heures
export const revalidate = 3600
export default async function BlogList() {
  const posts = await prisma.post.findMany({
    where: { published: true },
    orderBy: { createdAt: 'desc' }
  })
  return (
    <div>
      {posts.map(post => (
        <article key={post.id}>
          <h2>{post.title}</h2>
          <p>{post.content.slice(0, 150)}...</p>
        </article>
      ))}
    </div>
  )
}

HTML statique au build, régénération horaire — l’utilisateur reçoit un fichier statique, très rapide.

Détail d’article — SSR dynamique :

// app/blog/[slug]/page.tsx
export default async function Post({ params }: { params: { slug: string } }) {
  const post = await prisma.post.findUnique({
    where: { slug: params.slug }
  })
  if (!post) notFound()
  return (
    <article>
      <h1>{post.title}</h1>
      <div dangerouslySetInnerHTML={{ __html: post.content }} />
    </article>
  )
}

Chaque visite interroge la base — adapté aux contenus souvent mis à jour.

Comparaison des performances :

~200 ms
Temps de chargement SSG
Génération statique, le plus rapide
~800 ms
Temps de chargement SSR
Rendu serveur, contenu dynamique
~1500 ms
Temps de chargement CSR pur
Rendu client, SEO faible

L’écart parle de lui-même. Next.js pour un blog : bon SEO et bonnes perfs.

Déploiement et optimisations

Code prêt — déploiement.

Déploiement sur Vercel

Push GitHub, import du dépôt sur Vercel. Détection Next.js automatique. Variables à configurer manuellement :

DATABASE_URL="votre connexion base de production"
GITHUB_ID="OAuth Client ID"
GITHUB_SECRET="OAuth Secret"
NEXTAUTH_URL="https://your-domain.vercel.app"
NEXTAUTH_SECRET="chaîne aléatoire"

Base de prod : Supabase PostgreSQL gratuit, 500 Mo/mois — largement suffisant pour un blog perso.

Deploy : site en ligne en ~2 minutes. Première visite sur mon domaine : quelques secondes de silence, puis capture d’écran.

Optimisations production

Pool de connexions Prisma :

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
  // Obligatoire en serverless Vercel
  directUrl = env("DIRECT_URL")
}

En serverless, chaque invocation peut ouvrir une connexion ; directUrl réutilise le pool.

Images :

import Image from 'next/image'
<Image
  src="/avatar.jpg"
  alt="用户头像"
  width={100}
  height={100}
  // compression et optimisation automatiques Next.js
/>

WebP, chargement à la demande — gains visibles.

Scores finaux :

96
Score Lighthouse desktop
Mobile 92, SEO 100 — de zéro à la prod en deux jours

Lighthouse : desktop 96, mobile 92, SEO 100. De zéro à un site de niveau production en deux jours — motivant.

Extensions et ressources

Version MVP pour l’instant ; pistes d’évolution :

  • Éditeur Markdown : react-md-editor
  • Commentaires : Giscus (GitHub Discussions)
  • Recherche : Algolia ou full-text Prisma
  • Mode sombre : next-themes

Prochaine étape : un éditeur Markdown pour écrire plus vite.

Ressources :

Faire une fois vaut mieux que lire la doc dix fois. La théorie aide ; seul un projet réel ancre les compétences.

Conclusion

Ce week-end m’a appris :

  1. Puissance des Server Actions : elles remplacent la plupart des API Routes
  2. Typage Prisma : TypeScript complet, code plus serein
  3. Flexibilité SSG/SSR : adapter la stratégie de rendu selon la page
  4. Simplicité Vercel : du code à la prod en moins de 5 minutes

Le plus grand gain n’est pas technique — c’est la confiance : le full-stack n’est pas si inaccessible, à condition de s’y mettre.

Si vous voulez votre propre blog, lancez-vous. Les blocages sont normaux ; j’ai avancé doc ouverte, avec des erreurs. Voir le site en ligne compense tout.

J’espère que cet article vous fera gagner du temps. Questions en commentaires — je répondrai quand je peux.

Hâte de voir vos réalisations !

Construire un blog de niveau production avec Next.js 15

Du setup au déploiement : Server Actions, Prisma, optimisation SSG/SSR et fonctionnalités clés

Estimated time: PT16H

  1. 1

    Step 1: Installation et initialisation du projet

    Créer le projet Next.js 15 :
  2. 2

    Step 2: • Exécuter

    npx create-next-app@latest my-blog
  3. 3

    Step 3: Configurer le schema Prisma

    Schema principal :
  4. 4

    Step 4: • lib/prisma.ts

    instance globale unique
  5. 5

    Step 5: Implémenter les Server Actions

    Fichier app/actions/post-actions.ts :
  6. 6

    Step 6: Configurer l’authentification

    NextAuth.js déjà installé via npm
  7. 7

    Step 7: Optimiser SSR/SSG

    Liste : SSG+ISR dans app/blog/page.tsx avec export const revalidate = 3600, prisma.post.findMany sur les articles publiés, tri par createdAt desc. HTML statique au build, régénération horaire, chargement ~200 ms. Détail : SSR dynamique dans app/blog/[slug]/page.tsx, prisma.post.findUnique({ where: { slug: params.slug } }), contenu à jour à chaque visite, ~800 ms. Comparaison : SSG ~200 ms, SSR ~800 ms, CSR ~1500 ms.
  8. 8

    Step 8: Déployer sur Vercel et optimiser

    Push GitHub, import Vercel (détection Next.js auto). Variables : DATABASE_URL, GITHUB_ID, GITHUB_SECRET, NEXTAUTH_URL (https://your-domain.vercel.app), NEXTAUTH_SECRET. Base recommandée : Supabase PostgreSQL gratuit (500 Mo/mois). Prisma : directUrl = env(“DIRECT_URL”) dans datasource pour le pool en serverless. Images : composant Image Next.js, WebP et chargement à la demande. Deploy ~2 min ; Lighthouse desktop 96, mobile 92, SEO 100.

FAQ

Quelle différence entre Server Actions et API Routes dans Next.js 15 ? Quand utiliser l'un ou l'autre ?
Les Server Actions, c'est la livraison à domicile ; les API Routes, c'est aller chercher au restaurant. L'un est pratique, l'autre flexible.

Avantages des Server Actions :
1) Environ 30 % de code en moins, pas besoin d'API Routes
2) Appel direct depuis le composant, sans useState, fetch ni gestion onSubmit
3) Next.js gère sérialisation, requêtes réseau et erreurs
4) Typage sûr avec TypeScript

Exemple : ajoutez 'use server' avant la fonction, puis <form action={createPost}> dans le composant.

Avantages des API Routes :
• Plus flexibles pour une logique HTTP complexe
• Support des middlewares
• Adaptées quand vous devez personnaliser la réponse HTTP

Dans la plupart des cas, les Server Actions suffisent ; gardez les API Routes pour des besoins HTTP avancés.
Quel gain de performance avec Turbopack ? Quelle expérience concrète ?
Next.js 15 a promu Turbopack du statut expérimental à stable.

Chiffres officiels :
• Démarrage du serveur local : +76,7 %
• Mise à jour du code : +96,3 %

Mon retour terrain :
• Projet d'environ 30 composants : Webpack 7-8 s au démarrage, Turbopack sous 2 s
• Modification de code : hot reload quasi instantané
• Le confort de développement change vraiment la donne

Turbopack est écrit en Rust, bien plus rapide que Webpack, surtout sur les gros projets. Si vous êtes encore sur Webpack, passez à Next.js 15 pour tester Turbopack.
Pourquoi le singleton Prisma Client est-il important ? Que se passe-t-il sans lui ?
Le singleton Prisma Client est la bonne pratique recommandée par Prisma.

Problème :
• En dev, Next.js recharge à chaud et recrée des instances Prisma Client
• Le pool de connexions sature vite → erreur Too many connections
• Au premier déploiement sur Vercel, j'avais oublié cette config ; le site a tenu un moment puis a planté — j'ai cru à une attaque DDoS

Configuration correcte dans lib/prisma.ts :
const globalForPrisma = globalThis as unknown as { prisma: PrismaClient | undefined };
export const prisma = globalForPrisma.prisma ?? new PrismaClient();
if (process.env.NODE_ENV !== 'production') {
globalForPrisma.prisma = prisma;
}

En dev, une seule instance Prisma ; en production, comportement inchangé. À retenir absolument.
SSG, SSR, ISR : quelles différences et comment choisir ?
SSG (Static Site Generation) :
• HTML statique au build, le plus rapide (~200 ms)
• Pages peu changeantes (liste du blog, page À propos)

SSR (Server-Side Rendering) :
• HTML généré à chaque requête
• Données temps réel (détail d'article, profil utilisateur)
• Chargement ~800 ms

ISR (Incremental Static Regeneration) :
• SSG + régénération planifiée
• Contenu mis à jour de temps en temps (liste rafraîchie toutes les heures)

CSR pur (Client-Side Rendering) :
• Rendu dans le navigateur, SEO faible, ~1500 ms
• Peu adapté à un blog

Stratégie :
• Liste du blog : SSG + ISR (export const revalidate = 3600)
• Détail d'article : SSR dynamique
• Page À propos : SSG pur

Ordre de grandeur : SSG ~200 ms, SSR ~800 ms, CSR ~1500 ms.
Pourquoi PostgreSQL plutôt que MongoDB ? Quel support Prisma pour PostgreSQL ?
Raisons du choix PostgreSQL :
1) Meilleur support Prisma : typage, migrations, requêtes optimisées
2) Données relationnelles claires (articles, utilisateurs, commentaires)
3) Recherche full-text puissante pour un blog
4) Stabilité et transactions solides en production

Avantages Prisma :
• Types TypeScript générés depuis le schema
• IntelliSense complet, peu d'erreurs
• Après Mongoose sur MongoDB, la maintenance manuelle des types était pénible

Pour des relations complexes, PostgreSQL + Prisma est un excellent duo.
Que surveiller pour déployer Next.js sur Vercel ? Comment optimiser en production ?
Déploiement Vercel :
1) Pousser le code sur GitHub
2) Importer le dépôt — Vercel détecte Next.js et configure le projet
3) Variables d'environnement : DATABASE_URL, GITHUB_ID, GITHUB_SECRET, NEXTAUTH_URL, NEXTAUTH_SECRET
4) Deploy : site en ligne en ~2 minutes

Optimisations production :

1) Pool Prisma :
• Ajouter directUrl = env("DIRECT_URL") dans datasource de prisma/schema.prisma
• En serverless, chaque invocation peut ouvrir une connexion ; directUrl réutilise le pool

2) Images :
• Composant Image de Next.js : WebP, chargement à la demande, gains nets

3) Base : Supabase PostgreSQL gratuit (500 Mo/mois, largement suffisant pour un blog perso)

Résultat Lighthouse : desktop 96, mobile 92, SEO 100 — de zéro à la prod en deux jours.

10 min de lecture · Publié le: 24 nov. 2025 · Mis à jour le: 27 juil. 2026

Commentaires

Connectez-vous avec GitHub pour laisser un commentaire

Easton BlogEaston Blog