Faites tourner Next.js partout — même là où Node.js ne va pas. OpenNext est un adaptateur open source qui compile une application Next.js standard pour s'exécuter sur des plateformes serverless sans runtime Node.js complet, comme Cloudflare Workers. Avec l'adaptateur officiel @opennextjs/cloudflare, vous gardez l'expérience développeur de Next.js tout en gagnant des démarrages à froid sous la milliseconde, plus de 300 emplacements en périphérie, et des bindings directs vers D1, R2, KV et Durable Objects.
Ce que vous allez construire
Dans ce tutoriel, vous allez déployer une application Next.js 15 prête pour la production sur Cloudflare Workers grâce à OpenNext. L'application est un petit tableau de bord de style SaaS qui démontre toutes les fonctionnalités sur lesquelles vous comptez habituellement :
- App Router avec layouts imbriqués et React Server Components
- Server Actions qui écrivent dans une base de données Cloudflare D1
- Téléversement d'images vers R2 (stockage objet compatible S3)
- Régénération statique incrémentale (ISR) soutenue par Cloudflare KV
- Edge middleware pour l'authentification et l'i18n
- Routes dynamiques avec
generateStaticParamset revalidation à la demande - Streaming et Suspense avec Server Components
À la fin, vous aurez une application Next.js distribuée mondialement — moins chère, plus rapide et plus simple à exploiter qu'un déploiement Node.js traditionnel — sans réécrire une seule page.
Pourquoi OpenNext ?
Vercel est le chemin le plus simple pour expédier Next.js, mais ce n'est pas le seul. Auto-héberger Next.js sur Node.js fonctionne, mais cela nécessite un serveur toujours allumé, une latence régionale et une charge ops. OpenNext a été créé pour combler cet écart en traduisant la sortie de build de Next.js en bundles spécifiques à la plateforme.
L'adaptateur Cloudflare (@opennextjs/cloudflare) prend la sortie standalone de Next.js et la réécrit pour s'exécuter dans le runtime V8 isolate de Workers. Comparé à un hébergement Node.js traditionnel, vous obtenez :
- Démarrages à froid sous la milliseconde — les V8 isolates démarrent presque instantanément
- Plus de 300 points de présence — votre application tourne en périphérie dans le monde entier
- Bindings de plateforme directs — D1, R2, KV, Durable Objects, Queues, Vectorize accessibles en tant qu'objets JavaScript sans surcharge SDK
- Tarification qui s'effondre à zéro — payez à la requête, pas à l'instance en cours d'exécution
- Pas de Docker, pas de Kubernetes, pas de PM2 —
wrangler deployet vous êtes en ligne
OpenNext supporte Next.js 14 et Next.js 15, y compris Server Actions, Partial Prerendering et les fonctionnalités React 19 arrivées en 2025.
Prérequis
Avant de commencer, assurez-vous d'avoir :
- Node.js 20 ou plus récent (
node --version) - npm 10+, pnpm 9+ ou bun 1.2+
- Un compte Cloudflare (le plan gratuit suffit pour suivre)
- Le CLI Wrangler installé globalement :
npm install -g wrangler@latest - Une familiarité de base avec Next.js App Router et TypeScript
- Un éditeur de code (VS Code recommandé)
Authentifiez Wrangler auprès de votre compte Cloudflare une fois :
wrangler loginCela ouvre une fenêtre de navigateur et stocke les identifiants localement, de sorte que les commandes suivantes s'authentifient automatiquement.
Étape 1 : créer un nouveau projet Next.js
Démarrez avec un projet Next.js 15 vierge. L'équipe OpenNext maintient un template, mais pour la clarté nous partirons de zéro afin que vous compreniez chaque pièce.
npx create-next-app@latest noqta-edge-app \
--typescript \
--app \
--tailwind \
--eslint \
--src-dir \
--import-alias "@/*"
cd noqta-edge-appConfirmez que tout fonctionne en local :
npm run devOuvrez http://localhost:3000 et vous devriez voir la page d'accueil par défaut de Next.js. Arrêtez le serveur de dev avant de continuer.
Étape 2 : installer l'adaptateur OpenNext Cloudflare
Installez l'adaptateur et ses dépendances pairs :
npm install --save-dev @opennextjs/cloudflare wrangler@latest
npm install @cloudflare/workers-typesLe paquet @opennextjs/cloudflare contient à la fois l'adaptateur de build et des helpers pour accéder aux bindings Cloudflare depuis le code Next.js. Le paquet @cloudflare/workers-types fournit les types TypeScript pour D1, R2, KV, Queues et le reste de l'API Workers.
Ajoutez les types Workers à tsconfig.json pour que l'autocomplétion de l'éditeur fonctionne correctement :
{
"compilerOptions": {
"types": ["@cloudflare/workers-types"]
}
}Étape 3 : configurer wrangler.toml
Créez un fichier wrangler.toml à la racine du projet. Ce fichier indique à Wrangler quoi déployer, où le déployer et quelles ressources Cloudflare lier.
name = "noqta-edge-app"
main = ".open-next/worker.js"
compatibility_date = "2026-05-01"
compatibility_flags = ["nodejs_compat"]
# Requis par OpenNext : servir les assets statiques directement
[assets]
directory = ".open-next/assets"
binding = "ASSETS"
# Cache soutenu par KV — utilisé par ISR et le fetch cache
[[kv_namespaces]]
binding = "NEXT_INC_CACHE_KV"
id = "REPLACE_WITH_KV_ID"
# Base de données pour les Server Actions
[[d1_databases]]
binding = "DB"
database_name = "noqta-edge-db"
database_id = "REPLACE_WITH_D1_ID"
# Stockage objet pour les uploads
[[r2_buckets]]
binding = "UPLOADS"
bucket_name = "noqta-edge-uploads"La ligne compatibility_flags = ["nodejs_compat"] est critique. Elle active les API Node.js polyfillées de Cloudflare sur lesquelles Next.js s'appuie (Buffer, crypto, util, etc.). Sans cela, votre build s'exécutera, mais plusieurs internals de Next.js échoueront à l'exécution.
Étape 4 : créer les ressources Cloudflare
Provisionnez le namespace KV, la base de données D1 et le bucket R2 depuis la ligne de commande. Chaque commande affiche l'ID que vous devez recoller dans wrangler.toml.
# Namespace KV pour ISR / cache
wrangler kv namespace create NEXT_INC_CACHE_KV
# Base de données D1
wrangler d1 create noqta-edge-db
# Bucket R2
wrangler r2 bucket create noqta-edge-uploadsMettez à jour wrangler.toml avec les ID retournés par les deux premières commandes. Les buckets R2 sont référencés par nom, donc aucun ID n'est requis là.
Maintenant, créez un schéma de base de données. Sauvegardez le fichier suivant sous schema.sql :
CREATE TABLE IF NOT EXISTS posts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
body TEXT NOT NULL,
created_at INTEGER NOT NULL DEFAULT (unixepoch())
);
CREATE INDEX IF NOT EXISTS idx_posts_created_at
ON posts (created_at DESC);Appliquez-le localement et à distance :
# Appliquer à l'émulateur D1 local
wrangler d1 execute noqta-edge-db --local --file=schema.sql
# Appliquer à la base de données de production
wrangler d1 execute noqta-edge-db --remote --file=schema.sqlÉtape 5 : ajouter la configuration de build OpenNext
OpenNext a besoin d'un minuscule fichier de configuration pour savoir comment construire le projet. Créez open-next.config.ts à la racine du projet :
import { defineCloudflareConfig } from "@opennextjs/cloudflare";
import kvIncrementalCache from "@opennextjs/cloudflare/overrides/incremental-cache/kv-incremental-cache";
export default defineCloudflareConfig({
incrementalCache: kvIncrementalCache,
});Ce seul fichier branche le cache ISR basé sur KV. L'adaptateur livre plusieurs modules d'override (cache R2, cache régional, cache durable object) ; KV est le défaut et fonctionne pour la plupart des applications.
Mettez à jour package.json avec les scripts de build et de déploiement :
{
"scripts": {
"dev": "next dev",
"build": "next build",
"preview": "opennextjs-cloudflare build && wrangler dev",
"deploy": "opennextjs-cloudflare build && wrangler deploy"
}
}Le script build exécute le build standard de Next.js. Le script deploy exécute le build OpenNext (qui transforme la sortie Next.js en bundle Worker) puis l'expédie avec Wrangler.
Étape 6 : lire les bindings depuis Next.js
La façon la plus propre d'accéder aux bindings Cloudflare depuis les Server Components, Route Handlers ou Server Actions passe par le helper getCloudflareContext() d'OpenNext. Créez src/lib/cf.ts :
import { getCloudflareContext } from "@opennextjs/cloudflare";
import type { D1Database, R2Bucket, KVNamespace } from "@cloudflare/workers-types";
interface Env {
DB: D1Database;
UPLOADS: R2Bucket;
NEXT_INC_CACHE_KV: KVNamespace;
}
export async function getEnv(): Promise<Env> {
const ctx = await getCloudflareContext({ async: true });
return ctx.env as unknown as Env;
}Maintenant n'importe quel Server Component peut tirer l'environnement en une seule ligne :
const { DB } = await getEnv();Type-safe, zéro boilerplate, et entièrement typé contre les bindings de votre projet.
Étape 7 : construire une Server Action avec D1
Les Server Actions sont la façon la plus propre de muter des données dans Next.js 15, et elles fonctionnent parfaitement sur OpenNext. Créez src/app/posts/actions.ts :
"use server";
import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation";
import { getEnv } from "@/lib/cf";
export async function createPost(formData: FormData) {
const title = String(formData.get("title") ?? "").trim();
const body = String(formData.get("body") ?? "").trim();
if (!title || !body) {
throw new Error("Title and body are required");
}
const { DB } = await getEnv();
await DB.prepare(
"INSERT INTO posts (title, body) VALUES (?, ?)"
)
.bind(title, body)
.run();
revalidatePath("/posts");
redirect("/posts");
}Puis un Server Component qui liste et crée des posts dans src/app/posts/page.tsx :
import { getEnv } from "@/lib/cf";
import { createPost } from "./actions";
export const revalidate = 60;
interface Post {
id: number;
title: string;
body: string;
created_at: number;
}
export default async function PostsPage() {
const { DB } = await getEnv();
const result = await DB.prepare(
"SELECT id, title, body, created_at FROM posts ORDER BY created_at DESC LIMIT 50"
).all<Post>();
const posts = result.results ?? [];
return (
<main className="mx-auto max-w-2xl p-8">
<h1 className="text-3xl font-bold">Posts</h1>
<form action={createPost} className="mt-6 space-y-3">
<input
name="title"
placeholder="Title"
className="w-full rounded border p-2"
required
/>
<textarea
name="body"
placeholder="What's on your mind?"
className="w-full rounded border p-2 h-28"
required
/>
<button
type="submit"
className="rounded bg-orange-500 px-4 py-2 font-medium text-white"
>
Publish
</button>
</form>
<ul className="mt-10 space-y-6">
{posts.map((p) => (
<li key={p.id} className="rounded border p-4">
<h2 className="text-xl font-semibold">{p.title}</h2>
<p className="mt-2 text-gray-700">{p.body}</p>
</li>
))}
</ul>
</main>
);
}La directive export const revalidate = 60 est honorée par OpenNext : la page rendue est stockée dans KV et servie depuis le cache pendant jusqu'à 60 secondes, réduisant drastiquement les lectures D1 sous charge.
Étape 8 : téléverser des fichiers vers R2
R2 est le stockage objet de Cloudflare compatible S3 sans frais d'egress. Ajoutez un Route Handler dans src/app/api/upload/route.ts :
import { NextRequest, NextResponse } from "next/server";
import { getEnv } from "@/lib/cf";
export async function POST(req: NextRequest) {
const formData = await req.formData();
const file = formData.get("file");
if (!(file instanceof File)) {
return NextResponse.json({ error: "No file provided" }, { status: 400 });
}
const { UPLOADS } = await getEnv();
const key = `uploads/${crypto.randomUUID()}-${file.name}`;
await UPLOADS.put(key, await file.arrayBuffer(), {
httpMetadata: { contentType: file.type },
});
return NextResponse.json({
key,
url: `/api/files/${encodeURIComponent(key)}`,
});
}Et un handler de lecture correspondant dans src/app/api/files/[...key]/route.ts :
import { NextRequest } from "next/server";
import { getEnv } from "@/lib/cf";
export async function GET(
_req: NextRequest,
{ params }: { params: Promise<{ key: string[] }> }
) {
const { key } = await params;
const fullKey = key.join("/");
const { UPLOADS } = await getEnv();
const obj = await UPLOADS.get(fullKey);
if (!obj) {
return new Response("Not found", { status: 404 });
}
return new Response(obj.body, {
headers: {
"content-type": obj.httpMetadata?.contentType ?? "application/octet-stream",
"cache-control": "public, max-age=31536000, immutable",
},
});
}R2 stream les réponses directement à travers le Worker — pas de buffering, pas de double saut. Vous pouvez servir des assets supérieurs à 100 Mo sans difficulté.
Étape 9 : exécuter l'application en local sur Wrangler
Le serveur de dev de Next.js ne peut pas exposer les bindings Cloudflare, donc pour des tests bout-en-bout en local, utilisez Wrangler :
npm run previewCela exécute d'abord opennextjs-cloudflare build, qui produit un répertoire .open-next contenant le point d'entrée du Worker et les assets statiques. Wrangler sert ensuite le Worker sur http://localhost:8787, avec un accès complet aux émulateurs locaux de votre base D1, namespace KV et bucket R2.
Vous devriez voir des logs comme :
Building Next.js (production)...
Building OpenNext Cloudflare bundle...
Bundling worker.js (4.2 MB minified, 1.1 MB gzipped)
Ready on http://localhost:8787
Visitez la route /posts, soumettez le formulaire, et confirmez que les lignes apparaissent dans l'instance D1 locale :
wrangler d1 execute noqta-edge-db --local --command="SELECT * FROM posts;"Étape 10 : déployer en production
Quand tout est satisfaisant en local, expédiez en production avec une seule commande :
npm run deployWrangler téléverse le bundle, enregistre le Worker sur une URL *.workers.dev, et lie vos ressources KV, D1 et R2. En quelques secondes votre application Next.js tourne sur chaque centre de données Cloudflare de la planète.
Liez un domaine personnalisé via le tableau de bord Cloudflare (Workers and Pages, puis Custom Domains) ou via Wrangler :
wrangler deploy --routes "edge.noqta.tn/*"Cloudflare provisionne et renouvelle automatiquement les certificats TLS — pas de scripts Let's Encrypt à maintenir.
Cache, ISR et revalidatePath
OpenNext implémente le contrat de cache complet de Next.js sur Cloudflare KV :
- Les pages statiques sont écrites une fois au moment du build et poussées vers KV
- Les pages ISR (
export const revalidate = N) sont revalidées en arrière-plan à la première requête après expiration revalidatePath()etrevalidateTag()dans les Server Actions purgent instantanément les entrées KV concernées- Le fetch cache (
fetch(url, { next: { revalidate: 300 } })) est aussi stocké dans KV
Parce que KV est répliqué à chaque emplacement edge, un cache hit n'importe où dans le monde est servi en moins de 50 ms — généralement plus proche de 5 ms.
Si vous avez besoin de contenu dynamique à plus faible latence (par requête, sans cache), OpenNext expose aussi un override regional cache et un override Durable Object cache pour une invalidation fortement cohérente. Voir la documentation OpenNext pour les compromis.
Edge Middleware
Le middleware Next.js s'exécute à la périphérie par défaut — et avec OpenNext, cela signifie à l'intérieur du même Worker. Une simple porte d'authentification ressemble à un déploiement Vercel :
import { NextRequest, NextResponse } from "next/server";
export function middleware(req: NextRequest) {
const session = req.cookies.get("session");
if (!session && req.nextUrl.pathname.startsWith("/dashboard")) {
const loginUrl = new URL("/login", req.nextUrl);
loginUrl.searchParams.set("from", req.nextUrl.pathname);
return NextResponse.redirect(loginUrl);
}
return NextResponse.next();
}
export const config = {
matcher: ["/dashboard/:path*"],
};Pas d'imports spéciaux, pas de wrappers spécifiques à la plateforme — le même code qui tourne sur Vercel tourne sur Cloudflare Workers.
Observabilité
Cloudflare expédie une observabilité de premier ordre pour les Workers :
- Logs en temps réel avec
wrangler tail - Workers Analytics dans le tableau de bord (compteurs de requêtes, taux d'erreurs, latence p50/p99)
- Tail Workers pour transférer les logs vers Datadog, Logflare ou Honeycomb
- Support OpenTelemetry via le paquet
otel-cf-workers
Activez la journalisation structurée en utilisant simplement console.log() dans vos Server Components — Wrangler les capture avec horodatages à la milliseconde et IDs de requête.
Tester votre implémentation
Vérifiez que le déploiement fonctionne de bout en bout :
- Visitez votre URL de production et chargez
/posts - Soumettez le formulaire avec un nouveau titre et un nouveau corps
- Confirmez que le nouveau post apparaît (la Server Action a écrit dans D1)
- Consultez
wrangler tailpour le journal des requêtes en direct - Lancez
wrangler d1 execute noqta-edge-db --remote --command="SELECT COUNT(*) FROM posts;"et vérifiez le compteur de lignes - Téléversez un fichier via le endpoint
/api/uploadet relisez-le via/api/files/[key]
Si les six étapes passent, vous avez une application Next.js entièrement fonctionnelle sur Cloudflare Workers.
Dépannage
Le build échoue avec Cannot find module 'node:fs' — assurez-vous que compatibility_flags = ["nodejs_compat"] est défini dans wrangler.toml, et que votre compatibility_date est à 2024-09-23 ou après.
getCloudflareContext() échoue pendant le dev local Next.js — la commande next dev n'expose pas les bindings. Utilisez npm run preview (qui appelle wrangler dev) pour tout chemin de code qui touche D1, R2 ou KV pendant le développement.
Le bundle Worker dépasse 10 Mo — la limite du plan gratuit. Utilisez le réglage unstable_streamingFetch dans OpenNext pour tree-shaker les dépendances server-only inutilisées. Le plan payant relève la limite à 25 Mo.
Les pages ISR ne se rafraîchissent pas — confirmez que NEXT_INC_CACHE_KV est lié et que vous n'avez pas accidentellement défini dynamic = "force-static". Suivez le Worker et cherchez la clé de cache à chaque requête.
Les assets statiques renvoient 404 — le bloc [assets] dans wrangler.toml doit pointer sur .open-next/assets. Vérifiez que le dossier existe après le build.
Étapes suivantes
- Ajoutez l'authentification avec Better Auth ou Auth.js v5 — les deux fonctionnent sur Workers
- Streamez les complétions IA via Workers AI ou l'API OpenAI Realtime
- Déplacez les jobs longs vers Cloudflare Queues déclenchés depuis les Server Actions
- Ajoutez Vectorize pour la recherche sémantique et le RAG sur vos données D1
- Mettez en place des environnements Wrangler (
--env staging,--env production) pour des déploiements multi-étapes sûrs - Ajoutez un workflow GitHub Actions qui lance
npm run deployà chaque push surmain
Conclusion
OpenNext rend possible la prise de n'importe quelle application Next.js 15 — App Router, Server Actions, ISR, middleware, tout — et son exécution sur Cloudflare Workers avec un seul fichier de configuration et une seule commande de déploiement. Vous obtenez une distribution mondiale, des démarrages à froid instantanés et un accès direct aux primitives de plateforme de premier ordre comme D1, R2 et KV, tout en gardant l'expérience développeur standard de Next.js que votre équipe connaît déjà.
Pour un petit SaaS ou un site de contenu à fort trafic, cette stack est nettement moins chère qu'un hébergeur Node.js et significativement plus rapide qu'une fonction Vercel mono-région. Pour une charge entreprise, les mêmes patrons passent à des milliards de requêtes sans jamais toucher un cluster Kubernetes.
Le Next.js que vous connaissez, déployé partout. C'est la promesse OpenNext — et en 2026 elle est enfin une réalité de production.