Neon Serverless Postgres avec Next.js App Router : construire une application full-stack avec le branching de base de données

Postgres serverless qui scale à zéro. Neon est une plateforme Postgres entièrement gérée, conçue pour les applications serverless et edge modernes. Dans ce tutoriel, vous construirez un gestionnaire de favoris complet avec Next.js 15, le driver serverless Neon et le branching de base de données pour des déploiements preview sécurisés.
Ce que vous apprendrez
À la fin de ce tutoriel, vous saurez :
- Configurer une base de données Neon Postgres avec le connection pooling serverless
- Utiliser le driver serverless Neon (
@neondatabase/serverless) pour les requêtes HTTP - Construire un gestionnaire de favoris full-stack avec Next.js 15 App Router
- Implémenter des Server Actions pour les mutations de base de données
- Créer des branches de base de données pour les déploiements preview
- Configurer le connection pooling pour les performances en production
- Déployer avec une gestion correcte des variables d'environnement
Prérequis
Avant de commencer, assurez-vous d'avoir :
- Node.js 20+ installé (
node --version) - De l'expérience en TypeScript (types, async/await)
- Une familiarité avec Next.js (App Router, Server Components, Server Actions)
- Un compte Neon — offre gratuite sur neon.tech (pas de carte bancaire requise)
- Un éditeur de code — VS Code ou Cursor recommandé
Pourquoi Neon ?
PostgreSQL est la référence en matière de bases de données relationnelles, mais le Postgres traditionnel nécessite des serveurs toujours en fonctionnement. Neon change la donne avec une architecture serverless conçue de zéro :
| Fonctionnalité | Postgres traditionnel | Neon Serverless |
|---|---|---|
| Scaling | Scaling vertical manuel | Auto-scale à zéro, monte en charge à la demande |
| Cold starts | N/A (toujours en marche) | Moins de 500ms de temps de réveil |
| Branching | pg_dump + restore manuel | Branches instantanées en copy-on-write |
| Coût | Payer pour le temps d'inactivité | Payer uniquement le compute et le stockage utilisés |
| Modèle de connexion | Connexions TCP persistantes | Requêtes HTTP + pooling WebSocket |
| Compatibilité edge | Nécessite TCP (pas edge-friendly) | Fonctionne sur Vercel Edge, Cloudflare Workers |
La fonctionnalité phare est le branching de base de données — vous pouvez créer une copie instantanée de votre base de données de production pour chaque pull request, exactement comme les branches Git pour vos données.
Étape 1 : Créer un projet Neon
- Inscrivez-vous sur neon.tech et créez un nouveau projet
- Choisissez votre région (prenez-en une proche de votre cible de déploiement)
- Nommez votre projet
bookmarks-app - Neon crée une branche par défaut
mainavec une base de donnéesneondb
Après la création, vous verrez votre chaîne de connexion. Elle ressemble à ceci :
postgres://username:password@ep-cool-name-123456.us-east-2.aws.neon.tech/neondb?sslmode=require
Sauvegardez-la — vous en aurez besoin à l'étape suivante. Neon fournit deux chaînes de connexion :
- Connexion directe — pour les migrations et tâches d'administration
- Connexion poolée — pour les requêtes applicatives (utilise le connection pooling via PgBouncer)
La connexion poolée ajoute -pooler au nom d'hôte :
postgres://username:password@ep-cool-name-123456-pooler.us-east-2.aws.neon.tech/neondb?sslmode=require
Étape 2 : Créer le projet Next.js
Créez un nouveau projet Next.js 15 avec TypeScript :
npx create-next-app@latest bookmarks-app --typescript --tailwind --eslint --app --src-dir --use-npm
cd bookmarks-appInstallez le driver serverless Neon et Drizzle ORM pour des requêtes type-safe :
npm install @neondatabase/serverless drizzle-orm
npm install -D drizzle-kit dotenvPourquoi le driver serverless Neon ?
Le package @neondatabase/serverless remplace le driver traditionnel pg. Il communique via HTTP et WebSockets au lieu de TCP, ce qui signifie :
- Fonctionne dans les runtimes edge (Vercel Edge Functions, Cloudflare Workers)
- Pas de surcharge de connexion persistante — chaque requête est une requête HTTP stateless
- Connection pooling automatique via le proxy Neon
- Surcharge inférieure à la milliseconde comparée aux connexions TCP traditionnelles
Étape 3 : Configurer les variables d'environnement
Créez un fichier .env.local à la racine de votre projet :
# Base de données Neon
DATABASE_URL="postgres://username:password@ep-cool-name-123456-pooler.us-east-2.aws.neon.tech/neondb?sslmode=require"
DIRECT_DATABASE_URL="postgres://username:password@ep-cool-name-123456.us-east-2.aws.neon.tech/neondb?sslmode=require"Remplacez les valeurs par vos véritables chaînes de connexion Neon. DATABASE_URL utilise la connexion poolée pour les requêtes applicatives, tandis que DIRECT_DATABASE_URL utilise la connexion directe pour les migrations.
Étape 4 : Définir le schéma de base de données
Créez le fichier de schéma dans src/db/schema.ts :
import { pgTable, serial, text, varchar, timestamp, boolean, integer } from "drizzle-orm/pg-core";
export const bookmarks = pgTable("bookmarks", {
id: serial("id").primaryKey(),
title: varchar("title", { length: 255 }).notNull(),
url: text("url").notNull(),
description: text("description"),
tags: text("tags").array(),
isFavorite: boolean("is_favorite").default(false).notNull(),
clickCount: integer("click_count").default(0).notNull(),
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at").defaultNow().notNull(),
});
export type Bookmark = typeof bookmarks.$inferSelect;
export type NewBookmark = typeof bookmarks.$inferInsert;Ce schéma définit une table bookmarks avec :
- Un
idauto-incrémenté comme clé primaire titleeturlcomme champs obligatoiresdescriptionoptionnelle et tableau detags- Un booléen
isFavoritepour le filtrage rapide - Un entier
clickCountpour suivre la popularité - Des timestamps automatiques pour
createdAtetupdatedAt
Étape 5 : Configurer Drizzle avec le driver Neon
Créez la connexion à la base de données dans src/db/index.ts :
import { neon } from "@neondatabase/serverless";
import { drizzle } from "drizzle-orm/neon-http";
import * as schema from "./schema";
if (!process.env.DATABASE_URL) {
throw new Error("DATABASE_URL environment variable is not set");
}
const sql = neon(process.env.DATABASE_URL);
export const db = drizzle(sql, { schema });C'est le point d'intégration clé. Au lieu d'utiliser le pg Pool traditionnel, vous utilisez neon() pour créer un exécuteur SQL basé sur HTTP, puis vous le passez à l'adaptateur neon-http de Drizzle.
Comprendre le flux de connexion
Votre App → Requête HTTP → Proxy Neon (pooler) → Neon Postgres
Chaque requête est une requête HTTP stateless. Il n'y a pas de connexion à gérer, pas de pool à configurer, et pas de limites de connexion à craindre. Le proxy Neon gère le connection pooling de leur côté.
Étape 6 : Configurer Drizzle Kit pour les migrations
Créez drizzle.config.ts à la racine du projet :
import { defineConfig } from "drizzle-kit";
import dotenv from "dotenv";
dotenv.config({ path: ".env.local" });
export default defineConfig({
schema: "./src/db/schema.ts",
out: "./drizzle",
dialect: "postgresql",
dbCredentials: {
url: process.env.DIRECT_DATABASE_URL!,
},
});Notez que nous utilisons DIRECT_DATABASE_URL (pas la version poolée) pour les migrations. Les migrations nécessitent des instructions DDL qui fonctionnent mieux en connexion directe.
Ajoutez les scripts de migration dans package.json :
{
"scripts": {
"db:generate": "drizzle-kit generate",
"db:migrate": "drizzle-kit migrate",
"db:push": "drizzle-kit push",
"db:studio": "drizzle-kit studio"
}
}Maintenant, poussez le schéma vers votre base de données Neon :
npm run db:pushVous devriez voir une sortie confirmant la création de la table bookmarks. Vous pouvez vérifier en ouvrant Drizzle Studio :
npm run db:studioCela ouvre un navigateur de base de données visuel sur https://local.drizzle.studio où vous pouvez inspecter les tables et les données.
Étape 7 : Construire les Server Actions pour les opérations CRUD
Créez src/app/actions.ts pour toutes les opérations de base de données :
"use server";
import { db } from "@/db";
import { bookmarks } from "@/db/schema";
import { eq, desc, sql, ilike, or } from "drizzle-orm";
import { revalidatePath } from "next/cache";
export async function getBookmarks(search?: string) {
if (search) {
return db
.select()
.from(bookmarks)
.where(
or(
ilike(bookmarks.title, `%${search}%`),
ilike(bookmarks.url, `%${search}%`),
ilike(bookmarks.description, `%${search}%`)
)
)
.orderBy(desc(bookmarks.createdAt));
}
return db
.select()
.from(bookmarks)
.orderBy(desc(bookmarks.createdAt));
}
export async function createBookmark(formData: FormData) {
const title = formData.get("title") as string;
const url = formData.get("url") as string;
const description = formData.get("description") as string;
const tags = (formData.get("tags") as string)
?.split(",")
.map((t) => t.trim())
.filter(Boolean);
await db.insert(bookmarks).values({
title,
url,
description: description || null,
tags: tags?.length ? tags : null,
});
revalidatePath("/");
}
export async function toggleFavorite(id: number) {
const [bookmark] = await db
.select({ isFavorite: bookmarks.isFavorite })
.from(bookmarks)
.where(eq(bookmarks.id, id));
if (bookmark) {
await db
.update(bookmarks)
.set({ isFavorite: !bookmark.isFavorite })
.where(eq(bookmarks.id, id));
}
revalidatePath("/");
}
export async function incrementClickCount(id: number) {
await db
.update(bookmarks)
.set({
clickCount: sql`${bookmarks.clickCount} + 1`,
})
.where(eq(bookmarks.id, id));
}
export async function deleteBookmark(id: number) {
await db.delete(bookmarks).where(eq(bookmarks.id, id));
revalidatePath("/");
}
export async function getStats() {
const [result] = await db
.select({
total: sql<number>`count(*)`,
favorites: sql<number>`count(*) filter (where ${bookmarks.isFavorite} = true)`,
totalClicks: sql<number>`coalesce(sum(${bookmarks.clickCount}), 0)`,
})
.from(bookmarks);
return result;
}Chaque Server Action communique directement avec Neon via HTTP. Il n'y a pas de couche de route API — les Server Actions Next.js appellent la base de données directement depuis le serveur, et le driver Neon gère le transport HTTP.
Étape 8 : Construire les composants UI
Composant carte de favori
Créez src/components/bookmark-card.tsx :
"use client";
import { Bookmark } from "@/db/schema";
import { toggleFavorite, deleteBookmark, incrementClickCount } from "@/app/actions";
export function BookmarkCard({ bookmark }: { bookmark: Bookmark }) {
const handleClick = async () => {
await incrementClickCount(bookmark.id);
};
return (
<div className="border rounded-lg p-4 hover:shadow-md transition-shadow bg-white">
<div className="flex items-start justify-between">
<div className="flex-1 min-w-0">
<a
href={bookmark.url}
target="_blank"
rel="noopener noreferrer"
onClick={handleClick}
className="text-lg font-semibold text-blue-600 hover:text-blue-800 hover:underline truncate block"
>
{bookmark.title}
</a>
<p className="text-sm text-gray-500 truncate mt-1">{bookmark.url}</p>
</div>
<div className="flex gap-2 ml-4">
<button
onClick={() => toggleFavorite(bookmark.id)}
className="text-xl hover:scale-110 transition-transform"
title={bookmark.isFavorite ? "Retirer des favoris" : "Ajouter aux favoris"}
>
{bookmark.isFavorite ? "\u2605" : "\u2606"}
</button>
<button
onClick={() => deleteBookmark(bookmark.id)}
className="text-red-400 hover:text-red-600 text-sm"
title="Supprimer le favori"
>
Supprimer
</button>
</div>
</div>
{bookmark.description && (
<p className="text-gray-600 mt-2 text-sm line-clamp-2">
{bookmark.description}
</p>
)}
<div className="flex items-center justify-between mt-3">
<div className="flex gap-1 flex-wrap">
{bookmark.tags?.map((tag) => (
<span
key={tag}
className="px-2 py-0.5 bg-gray-100 text-gray-600 rounded-full text-xs"
>
{tag}
</span>
))}
</div>
<span className="text-xs text-gray-400">
{bookmark.clickCount} clics
</span>
</div>
</div>
);
}Formulaire d'ajout de favori
Créez src/components/add-bookmark-form.tsx :
"use client";
import { createBookmark } from "@/app/actions";
import { useRef } from "react";
export function AddBookmarkForm() {
const formRef = useRef<HTMLFormElement>(null);
const handleSubmit = async (formData: FormData) => {
await createBookmark(formData);
formRef.current?.reset();
};
return (
<form ref={formRef} action={handleSubmit} className="space-y-4 bg-white p-6 rounded-lg border">
<h2 className="text-lg font-semibold">Ajouter un nouveau favori</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label htmlFor="title" className="block text-sm font-medium text-gray-700 mb-1">
Titre *
</label>
<input
type="text"
id="title"
name="title"
required
className="w-full px-3 py-2 border rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
placeholder="Mon favori"
/>
</div>
<div>
<label htmlFor="url" className="block text-sm font-medium text-gray-700 mb-1">
URL *
</label>
<input
type="url"
id="url"
name="url"
required
className="w-full px-3 py-2 border rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
placeholder="https://example.com"
/>
</div>
</div>
<div>
<label htmlFor="description" className="block text-sm font-medium text-gray-700 mb-1">
Description
</label>
<textarea
id="description"
name="description"
rows={2}
className="w-full px-3 py-2 border rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
placeholder="Une brève description..."
/>
</div>
<div>
<label htmlFor="tags" className="block text-sm font-medium text-gray-700 mb-1">
Tags (séparés par des virgules)
</label>
<input
type="text"
id="tags"
name="tags"
className="w-full px-3 py-2 border rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
placeholder="react, tutoriel, database"
/>
</div>
<button
type="submit"
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors"
>
Ajouter le favori
</button>
</form>
);
}Étape 9 : Construire la page principale
Mettez à jour src/app/page.tsx :
import { getBookmarks, getStats } from "./actions";
import { BookmarkCard } from "@/components/bookmark-card";
import { AddBookmarkForm } from "@/components/add-bookmark-form";
export default async function Home({
searchParams,
}: {
searchParams: Promise<{ search?: string }>;
}) {
const { search } = await searchParams;
const [allBookmarks, stats] = await Promise.all([
getBookmarks(search),
getStats(),
]);
return (
<main className="max-w-4xl mx-auto px-4 py-8">
<h1 className="text-3xl font-bold mb-2">Gestionnaire de favoris</h1>
<p className="text-gray-500 mb-8">
Propulsé par Neon Serverless Postgres
</p>
{/* Statistiques */}
<div className="grid grid-cols-3 gap-4 mb-8">
<div className="bg-blue-50 rounded-lg p-4 text-center">
<div className="text-2xl font-bold text-blue-600">{stats.total}</div>
<div className="text-sm text-gray-600">Total des favoris</div>
</div>
<div className="bg-yellow-50 rounded-lg p-4 text-center">
<div className="text-2xl font-bold text-yellow-600">{stats.favorites}</div>
<div className="text-sm text-gray-600">Favoris</div>
</div>
<div className="bg-green-50 rounded-lg p-4 text-center">
<div className="text-2xl font-bold text-green-600">{stats.totalClicks}</div>
<div className="text-sm text-gray-600">Total des clics</div>
</div>
</div>
{/* Formulaire */}
<div className="mb-8">
<AddBookmarkForm />
</div>
{/* Recherche */}
<form className="mb-6">
<input
type="text"
name="search"
defaultValue={search}
placeholder="Rechercher des favoris..."
className="w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
/>
</form>
{/* Liste des favoris */}
<div className="space-y-4">
{allBookmarks.length === 0 ? (
<div className="text-center py-12 text-gray-400">
<p className="text-lg">Aucun favori pour le moment</p>
<p className="text-sm mt-1">Ajoutez votre premier favori ci-dessus</p>
</div>
) : (
allBookmarks.map((bookmark) => (
<BookmarkCard key={bookmark.id} bookmark={bookmark} />
))
)}
</div>
</main>
);
}Lancez le serveur de développement pour vérifier que tout fonctionne :
npm run devOuvrez http://localhost:3000 et vous devriez voir le gestionnaire de favoris. Essayez d'ajouter quelques favoris, de les marquer comme favoris et de faire une recherche.
Étape 10 : Branching de base de données pour les déploiements preview
C'est là que Neon brille vraiment. Le branching de base de données crée un clone instantané en copy-on-write de votre base de données — parfait pour les déploiements preview, les tests de migration ou l'expérimentation avec les données.
Comment fonctionne le branching
Neon utilise une couche de stockage en copy-on-write inspirée de Git :
branche main (données de production)
└── preview/feature-xyz (copie instantanée, modifications isolées)
- Création instantanée — les branches sont créées en millisecondes, quelle que soit la taille de la base
- Zéro surcharge de stockage — les branches partagent les pages de données jusqu'à modification
- Isolation complète — les modifications sur une branche n'affectent jamais le parent
- Nettoyage automatique — les branches peuvent être supprimées quand la PR est fusionnée
Créer une branche via l'API Neon
Vous pouvez automatiser la création de branches dans votre pipeline CI/CD. Voici comment créer une branche avec l'API Neon :
// scripts/create-neon-branch.ts
const NEON_API_KEY = process.env.NEON_API_KEY!;
const NEON_PROJECT_ID = process.env.NEON_PROJECT_ID!;
async function createBranch(branchName: string) {
const response = await fetch(
`https://console.neon.tech/api/v2/projects/${NEON_PROJECT_ID}/branches`,
{
method: "POST",
headers: {
Authorization: `Bearer ${NEON_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
branch: {
name: branchName,
parent_id: undefined,
},
endpoints: [
{
type: "read_write",
},
],
}),
}
);
const data = await response.json();
const endpoint = data.endpoints[0];
const host = endpoint.host;
const dbName = "neondb";
const role = data.roles?.[0]?.name || "neondb_owner";
console.log(`Branche créée : ${data.branch.name}`);
console.log(`Connexion : postgres://${role}@${host}/${dbName}?sslmode=require`);
return data;
}
const prNumber = process.argv[2];
if (prNumber) {
createBranch(`preview/pr-${prNumber}`);
}Intégration GitHub Actions
Ajoutez ce workflow pour créer automatiquement une branche Neon pour chaque pull request :
# .github/workflows/preview-branch.yml
name: Create Preview Database
on:
pull_request:
types: [opened, synchronize]
jobs:
create-branch:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Create Neon Branch
uses: neondatabase/create-branch-action@v5
id: create-branch
with:
project_id: ${{ secrets.NEON_PROJECT_ID }}
api_key: ${{ secrets.NEON_API_KEY }}
branch_name: preview/pr-${{ github.event.pull_request.number }}
- name: Set DATABASE_URL
run: |
echo "Preview database URL: ${{ steps.create-branch.outputs.db_url_with_pooler }}"Quand la PR est fusionnée ou fermée, ajoutez un job de nettoyage :
# .github/workflows/cleanup-branch.yml
name: Cleanup Preview Database
on:
pull_request:
types: [closed]
jobs:
delete-branch:
runs-on: ubuntu-latest
steps:
- name: Delete Neon Branch
uses: neondatabase/delete-branch-action@v3
with:
project_id: ${{ secrets.NEON_PROJECT_ID }}
api_key: ${{ secrets.NEON_API_KEY }}
branch: preview/pr-${{ github.event.pull_request.number }}Cela vous donne de véritables environnements preview de base de données — chaque PR obtient sa propre base de données avec les données de production, complètement isolée de la production.
Étape 11 : Connection pooling et performances
Comprendre les modes de connexion Neon
Neon propose trois modes de connexion, chacun adapté à différents cas d'usage :
1. HTTP (Driver serverless)
import { neon } from "@neondatabase/serverless";
const sql = neon(process.env.DATABASE_URL);
const result = await sql`SELECT * FROM bookmarks`;Idéal pour : les fonctions serverless, les runtimes edge, les requêtes ponctuelles.
2. WebSocket (Poolé)
import { Pool } from "@neondatabase/serverless";
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
const { rows } = await pool.query("SELECT * FROM bookmarks");Idéal pour : les processus Node.js de longue durée, les requêtes multiples par requête.
3. TCP direct
import { Client } from "pg";
const client = new Client(process.env.DIRECT_DATABASE_URL);
await client.connect();Idéal pour : les migrations, les tâches d'administration, le développement local.
Choisir le bon mode pour Next.js
Pour une application Next.js App Router :
| Contexte | Mode recommandé |
|---|---|
| Server Components | HTTP (driver serverless neon) |
| Server Actions | HTTP (driver serverless neon) |
| Route Handlers | HTTP ou WebSocket Pool |
| Middleware (Edge) | HTTP uniquement |
| Migrations | TCP direct |
Conseils de performance
1. Utilisez Promise.all pour les requêtes parallèles :
// Mauvais : requêtes séquentielles (lent)
const bookmarks = await db.select().from(bookmarksTable);
const stats = await db.select().from(statsTable);
// Bon : requêtes parallèles (rapide)
const [bookmarks, stats] = await Promise.all([
db.select().from(bookmarksTable),
db.select().from(statsTable),
]);2. Sélectionnez uniquement les colonnes nécessaires :
// Mauvais : récupère toutes les colonnes
const result = await db.select().from(bookmarks);
// Bon : récupère uniquement ce dont vous avez besoin
const result = await db
.select({
id: bookmarks.id,
title: bookmarks.title,
url: bookmarks.url,
})
.from(bookmarks);3. Utilisez l'option fetchConnectionCache de Neon :
const sql = neon(process.env.DATABASE_URL, {
fetchConnectionCache: true,
});Cela réutilise la connexion fetch sous-jacente entre les requêtes dans la même requête, réduisant la latence de 10 à 20ms par requête.
Étape 12 : Ajouter la recherche full-text avec Postgres
Puisque nous utilisons du vrai Postgres, nous avons la recherche full-text intégrée — pas besoin de service de recherche externe :
Créez une migration pour ajouter un index GIN pour la recherche full-text :
-- drizzle/0001_add_search_index.sql
CREATE INDEX idx_bookmarks_search ON bookmarks
USING GIN (to_tsvector('english', coalesce(title, '') || ' ' || coalesce(description, '')));Mettez à jour l'action de recherche pour utiliser la recherche full-text de Postgres :
export async function searchBookmarks(query: string) {
const results = await db.execute(
sql`SELECT *, ts_rank(
to_tsvector('english', coalesce(title, '') || ' ' || coalesce(description, '')),
plainto_tsquery('english', ${query})
) as rank
FROM bookmarks
WHERE to_tsvector('english', coalesce(title, '') || ' ' || coalesce(description, ''))
@@ plainto_tsquery('english', ${query})
ORDER BY rank DESC`
);
return results.rows;
}Cela vous donne une recherche classée par pertinence alimentée par Postgres — pas besoin d'Algolia, Meilisearch ou Elasticsearch.
Dépannage
Problèmes courants
"Connection terminated unexpectedly"
Cela arrive généralement quand on utilise la connexion directe dans un environnement serverless. Passez à l'URL de connexion poolée (avec -pooler dans le nom d'hôte).
"Too many connections"
Si vous voyez des erreurs de limite de connexion, assurez-vous d'utiliser le driver HTTP (neon()) au lieu de la classe Pool. Le driver HTTP ne maintient pas de connexions ouvertes.
"Endpoint is suspended"
L'offre gratuite de Neon suspend les endpoints après 5 minutes d'inactivité. La première requête après la suspension prend 300-500ms pour se réveiller. Pour la production, passez à une offre payante avec des endpoints toujours actifs.
Échec des migrations
Utilisez toujours DIRECT_DATABASE_URL pour exécuter les migrations. La connexion poolée via PgBouncer ne supporte pas bien les instructions DDL.
Prochaines étapes
Maintenant que vous avez une application fonctionnelle alimentée par Neon, voici des pistes pour l'étendre :
- Ajoutez l'authentification avec Auth.js ou Better Auth pour des favoris par utilisateur
- Implémentez la sécurité au niveau des lignes avec les politiques Postgres RLS
- Configurez l'autoscaling Neon pour gérer les pics de trafic automatiquement
- Créez une API REST avec les Route Handlers pour les intégrations externes
- Ajoutez les mises à jour en temps réel avec la réplication logique Neon et les WebSockets
- Intégrez avec Vercel pour les déploiements preview automatiques avec les branches de base de données
Conclusion
Dans ce tutoriel, vous avez construit un gestionnaire de favoris full-stack alimenté par Neon Serverless Postgres et Next.js 15 App Router. Vous avez appris à :
- Configurer Neon avec le driver HTTP serverless pour des requêtes compatibles edge
- Construire des opérations de base de données type-safe avec Drizzle ORM
- Implémenter des opérations CRUD avec les Server Actions Next.js
- Créer des branches de base de données pour des environnements preview isolés
- Optimiser les performances avec le connection pooling et les requêtes parallèles
- Ajouter la recherche full-text Postgres sans dépendances externes
Neon apporte la puissance de PostgreSQL au monde serverless — l'auto-scaling, le branching instantané et la tarification à l'usage en font un excellent choix pour les applications full-stack modernes. Combiné avec Next.js App Router et Drizzle ORM, vous obtenez une expérience de développement à la fois productive et prête pour la production.
Discutez de votre projet avec nous
Nous sommes ici pour vous aider avec vos besoins en développement Web. Planifiez un appel pour discuter de votre projet et comment nous pouvons vous aider.
Trouvons les meilleures solutions pour vos besoins.
Articles connexes

Recherche plein texte PostgreSQL avec Next.js — Construire une recherche puissante sans Elasticsearch (2026)
Apprenez à construire une recherche plein texte rapide et tolérante aux fautes de frappe en utilisant les capacités intégrées de PostgreSQL avec Next.js App Router. Pas besoin d'Elasticsearch ou d'Algolia — juste votre base de données Postgres existante.

Construire une Application Full-Stack avec Drizzle ORM et Next.js 15 : Base de Donnees Type-Safe du Zero a la Production
Apprenez a construire une application full-stack type-safe avec Drizzle ORM et Next.js 15. Ce tutoriel pratique couvre la conception de schemas, les migrations, les Server Actions, les operations CRUD et le deploiement avec PostgreSQL.

Zustand + Next.js App Router : Gestion d'État React Moderne du Zéro à la Production
Maîtrisez la gestion d'état React moderne avec Zustand et Next.js 15 App Router. Ce tutoriel pratique couvre la création de stores, les middleware, la persistance, l'hydratation côté serveur et les patterns concrets pour des applications évolutives.