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

AI Bot
Par AI Bot ·

Chargement du lecteur de synthèse vocale...

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 traditionnelNeon Serverless
ScalingScaling vertical manuelAuto-scale à zéro, monte en charge à la demande
Cold startsN/A (toujours en marche)Moins de 500ms de temps de réveil
Branchingpg_dump + restore manuelBranches instantanées en copy-on-write
CoûtPayer pour le temps d'inactivitéPayer uniquement le compute et le stockage utilisés
Modèle de connexionConnexions TCP persistantesRequêtes HTTP + pooling WebSocket
Compatibilité edgeNé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

  1. Inscrivez-vous sur neon.tech et créez un nouveau projet
  2. Choisissez votre région (prenez-en une proche de votre cible de déploiement)
  3. Nommez votre projet bookmarks-app
  4. Neon crée une branche par défaut main avec une base de données neondb

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-app

Installez le driver serverless Neon et Drizzle ORM pour des requêtes type-safe :

npm install @neondatabase/serverless drizzle-orm
npm install -D drizzle-kit dotenv

Pourquoi 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 id auto-incrémenté comme clé primaire
  • title et url comme champs obligatoires
  • description optionnelle et tableau de tags
  • Un booléen isFavorite pour le filtrage rapide
  • Un entier clickCount pour suivre la popularité
  • Des timestamps automatiques pour createdAt et updatedAt

É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:push

Vous devriez voir une sortie confirmant la création de la table bookmarks. Vous pouvez vérifier en ouvrant Drizzle Studio :

npm run db:studio

Cela 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 dev

Ouvrez 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 :

ContexteMode recommandé
Server ComponentsHTTP (driver serverless neon)
Server ActionsHTTP (driver serverless neon)
Route HandlersHTTP ou WebSocket Pool
Middleware (Edge)HTTP uniquement
MigrationsTCP 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.


Vous voulez lire plus de tutoriels? Découvrez notre dernier tutoriel sur Construire des systemes multi-agents IA avec n8n : Guide complet d'automatisation intelligente.

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