écrits/tutorial/2026/07
Tutorial10 juil. 2026·28 min

RedwoodSDK : créer une application React full-stack sur Cloudflare Workers

Découvrez RedwoodSDK, le framework React conçu pour Cloudflare. Construisez une application complète de partage de snippets avec les React Server Components, les fonctions serveur, D1, les middlewares, les sessions Durable Object et un déploiement en une seule commande.

La plupart des méta-frameworks React ont été conçus pour des serveurs Node.js, puis adaptés après coup à l'edge. RedwoodSDK prend le chemin inverse. Il démarre comme un plugin Vite, ne cible que Cloudflare Workers comme runtime, et vous remet les objets Request et Response bruts au lieu de les dissimuler derrière des conventions. Les React Server Components, les fonctions serveur, le streaming et le temps réel reposent sur cette fondation plutôt que de coexister à côté d'elle.

Dans ce tutoriel, vous allez construire Snippets, une petite application de partage de code : une page d'accueil qui liste des snippets depuis une base Cloudflare D1, une page de détail sur une route dynamique, un composant client qui envoie de nouveaux snippets via une fonction serveur, un middleware qui protège une route d'administration, et des sessions par cookie adossées à un Durable Object. Puis vous déploierez le tout sur Cloudflare en une seule commande.

À la fin, vous saurez exactement où se situe la frontière serveur/client de RedwoodSDK, comment ctx circule au fil d'une requête, et pourquoi « ce n'est qu'un Worker » est la décision de conception la plus importante du framework.

Pourquoi RedwoodSDK, et quand y recourir

RedwoodSDK n'est pas un remplaçant universel de Next.js. Il fait un pari précis : si vous déployez de toute façon sur Cloudflare, le framework doit exposer la plateforme plutôt que l'abstraire. Ce pari a des conséquences qu'il vaut mieux comprendre avant de s'engager.

  • Aucun cycle de vie de requête caché. Votre application est un unique appel à defineApp qui reçoit une requête et renvoie une réponse. Les routes sont des fonctions. Les middlewares sont des fonctions. Si vous savez lire un Worker, vous savez lire une application RedwoodSDK.
  • Les primitives de la plateforme sont natives, pas des adaptateurs. D1, R2, KV, Queues et Durable Objects sont accessibles via le binding standard env importé depuis cloudflare:workers. Aucune couche de compatibilité ne traduit une API Node en API Cloudflare.
  • Le développement local reflète la production. Comme vite dev exécute votre code dans le véritable runtime Workers via Miniflare, la base D1 que vous interrogez en local se comporte comme celle que vous interrogez en production.
  • Le document HTML vous appartient. Il n'y a pas de layout racine magique. Vous écrivez vous-même un composant Document qui émet chaque balise.

Choisissez RedwoodSDK quand Cloudflare est votre cible de déploiement et que vous voulez un contrôle total sur les requêtes, les réponses et le streaming. Choisissez un framework plus large quand il vous faut un vaste écosystème de plugins, une optimisation d'images intégrée, ou la possibilité de changer d'hébergeur le trimestre prochain sans tout réécrire.

Prérequis

Avant de commencer, assurez-vous d'avoir :

  • Node.js 20 ou plus et pnpm installés (npm fonctionne aussi, mais la documentation RedwoodSDK utilise pnpm)
  • Un compte Cloudflare gratuit — indispensable pour créer une base D1 et pour déployer
  • Une bonne maîtrise des fondamentaux de React 19 : composants, props et hooks
  • Une compréhension pratique des React Server Components : des composants rendus sur le serveur, dont le JavaScript n'est jamais envoyé au navigateur
  • Un éditeur de code et un terminal

Aucune expérience préalable de Cloudflare Workers n'est nécessaire. Le tutoriel introduit chaque binding au moment où il devient utile.

Ce que vous allez construire

Snippets est un seul Worker exposant quatre routes :

RouteTypeRôle
/Server ComponentListe tous les snippets depuis D1
/s/:keyServer ComponentAffiche un snippet à partir de sa clé
/newServer Component + îlot clientFormulaire appelant une fonction serveur
/adminRoute protégéeBloquée par un middleware sans session

Le code applicatif tient en quelques centaines de lignes. Chacune s'exécute sur le réseau edge de Cloudflare.

Étape 1 : initialiser le projet

RedwoodSDK fournit un générateur de projet. Créez l'application et installez les dépendances :

npx create-rwsdk snippets
cd snippets
pnpm install
pnpm dev

Ouvrez l'URL localhost affichée. La page de démarrage doit apparaître. Observez maintenant ce que le générateur a produit — les parties intéressantes sont peu nombreuses :

snippets/
├── src/
│   ├── worker.tsx          # point d'entrée : defineApp vit ici
│   ├── client.tsx          # point d'hydratation des composants client
│   └── app/
│       ├── Document.tsx    # la coquille HTML que vous contrôlez
│       └── pages/
│           └── Home.tsx
├── wrangler.jsonc          # configuration Cloudflare et bindings
└── vite.config.mts

Voilà toute la surface du framework. Pas de convention de dossier app/, pas de routeur basé sur les fichiers, pas de manifeste de build à décoder.

Étape 2 : comprendre worker.tsx et le Document

Ouvrez src/worker.tsx. Ce fichier est le gestionnaire de requêtes de toute votre application :

// src/worker.tsx
import { defineApp } from "rwsdk/worker";
import { route, render } from "rwsdk/router";
 
import { Document } from "@/app/Document";
import { Home } from "@/app/pages/Home";
 
export default defineApp([
  render(Document, [
    route("/", Home),
  ]),
]);

Trois fonctions font tout le travail :

  • defineApp(handlers) prend un tableau de gestionnaires et renvoie un Worker. Les gestionnaires s'exécutent de haut en bas.
  • route(path, handler) associe un chemin. Le gestionnaire peut renvoyer un élément React, une Response, ou n'importe quoi entre les deux.
  • render(Document, routes) enveloppe un groupe de routes afin que tout élément React qu'elles renvoient soit rendu dans le document HTML fourni, puis diffusé en streaming vers le navigateur.

L'idée clé : les gestionnaires de route sont des fonctions ordinaires renvoyant des réponses ordinaires. Rien ne vous impose React :

export default defineApp([
  // Une réponse texte brute — React n'intervient pas
  route("/health", () => new Response("ok")),
 
  // Une réponse XML
  route("/sitemap.xml", async () => {
    const xml = await buildSitemap();
    return new Response(xml, {
      status: 200,
      headers: { "Content-Type": "application/xml" },
    });
  }),
 
  // Une redirection
  route("/docs", () =>
    new Response(null, {
      status: 301,
      headers: { Location: "https://docs.rwsdk.com" },
    })
  ),
 
  // Des routes React, enveloppées dans le Document
  render(Document, [route("/", Home)]),
]);

Ouvrez maintenant src/app/Document.tsx. Contrairement à un layout racine Next.js, il s'agit littéralement de la coquille de la page, et vous en écrivez chaque balise :

// src/app/Document.tsx
export const Document = ({ children }: { children: React.ReactNode }) => (
  <html lang="fr">
    <head>
      <meta charSet="utf-8" />
      <meta name="viewport" content="width=device-width, initial-scale=1" />
      <title>Snippets</title>
      <link rel="modulepreload" href="/src/client.tsx" />
    </head>
    <body>
      <div id="root">{children}</div>
      <script>import("/src/client.tsx")</script>
    </body>
  </html>
);

Cette dernière balise script charge src/client.tsx, qui appelle initClient() pour hydrater les composants client de la page. Si vous la supprimez, votre application continue de s'afficher — elle devient simplement du HTML entièrement statique, sans interactivité. C'est une propriété utile à pouvoir raisonner.

// src/client.tsx
import { initClient } from "rwsdk/client";
 
initClient();

Étape 3 : écrire votre premier Server Component

Remplacez src/app/pages/Home.tsx par un Server Component. Comme il s'exécute sur le serveur, il peut lire tout ce que le Worker peut lire — sans useEffect, sans cascade de requêtes, sans état de chargement.

RedwoodSDK transmet à chaque page un objet RequestInfo en props, contenant request, params, ctx et response :

// src/app/pages/Home.tsx
import type { RequestInfo } from "rwsdk/worker";
 
export function Home({ request }: RequestInfo) {
  const url = new URL(request.url);
 
  return (
    <main>
      <h1>Snippets</h1>
      <p>Serving from {url.hostname}</p>
      <a href="/new">Create a snippet</a>
    </main>
  );
}

Important : dans un Server Component, récupérez requestInfo depuis les props. N'appelez pas getRequestInfo() à cet endroit — cet utilitaire existe pour les fonctions serveur, qui ne reçoivent aucune prop. L'appeler dans un Server Component lève une erreur à l'exécution. C'est l'erreur la plus fréquente chez les débutants.

Les segments dynamiques fonctionnent comme prévu et atterrissent dans params :

// src/worker.tsx
route("/s/:key", SnippetPage),
// src/app/pages/SnippetPage.tsx
import type { RequestInfo } from "rwsdk/worker";
 
export function SnippetPage({ params }: RequestInfo) {
  return <h1>Snippet: {params.key}</h1>;
}

Étape 4 : ajouter une base de données avec D1

Cloudflare D1 est une base SQLite qui s'exécute à l'edge. Créez-en une :

npx wrangler d1 create snippets-db

Wrangler affiche un database_id. Ajoutez le binding dans wrangler.jsonc :

{
  "name": "snippets",
  "main": "src/worker.tsx",
  "compatibility_date": "2026-01-01",
  "compatibility_flags": ["nodejs_compat"],
  "d1_databases": [
    {
      "binding": "DB",
      "database_name": "snippets-db",
      "database_id": "paste-the-id-wrangler-printed"
    }
  ]
}

Créez un fichier de migration dans migrations/0001_create_snippets.sql :

CREATE TABLE IF NOT EXISTS snippets (
  key        TEXT PRIMARY KEY,
  title      TEXT NOT NULL,
  language   TEXT NOT NULL DEFAULT 'text',
  body       TEXT NOT NULL,
  created_at INTEGER NOT NULL
);

Appliquez-la à la base locale, puis à la production :

# base locale (Miniflare) utilisée par pnpm dev
npx wrangler d1 migrations apply snippets-db --local
 
# la vraie base D1 distante
npx wrangler d1 migrations apply snippets-db --remote

Écrivez maintenant une fine couche d'accès aux données. L'objet env provient de cloudflare:workers et est entièrement typé :

// src/db.ts
import { env } from "cloudflare:workers";
 
export type Snippet = {
  key: string;
  title: string;
  language: string;
  body: string;
  created_at: number;
};
 
export async function listSnippets(): Promise<Snippet[]> {
  const { results } = await env.DB
    .prepare("SELECT * FROM snippets ORDER BY created_at DESC LIMIT 50")
    .all<Snippet>();
 
  return results;
}
 
export async function getSnippet(key: string): Promise<Snippet | null> {
  return await env.DB
    .prepare("SELECT * FROM snippets WHERE key = ?")
    .bind(key)
    .first<Snippet>();
}
 
export async function createSnippet(input: Omit<Snippet, "created_at">) {
  await env.DB
    .prepare(
      "INSERT INTO snippets (key, title, language, body, created_at) VALUES (?, ?, ?, ?, ?)"
    )
    .bind(input.key, input.title, input.language, input.body, Date.now())
    .run();
}

Remarquez les appels à .bind(). Les requêtes préparées de D1 sont paramétrées, et c'est précisément ce qui écarte l'injection SQL — n'interpolez jamais une saisie utilisateur dans une chaîne de requête.

Reliez ces requêtes à vos pages. C'est ici que les Server Components prennent tout leur sens : l'appel à la base a lieu pendant le rendu, sur la même machine, sans aller-retour vers le client.

// src/app/pages/Home.tsx
import { listSnippets } from "@/db";
 
export async function Home() {
  const snippets = await listSnippets();
 
  return (
    <main>
      <h1>Snippets</h1>
      <a href="/new">Create a snippet</a>
 
      <ul>
        {snippets.map((s) => (
          <li key={s.key}>
            <a href={`/s/${s.key}`}>{s.title}</a>
            <span> — {s.language}</span>
          </li>
        ))}
      </ul>
    </main>
  );
}
// src/app/pages/SnippetPage.tsx
import type { RequestInfo } from "rwsdk/worker";
import { getSnippet } from "@/db";
 
export async function SnippetPage({ params }: RequestInfo) {
  const snippet = await getSnippet(params.key);
 
  if (!snippet) {
    return new Response("Not found", { status: 404 });
  }
 
  return (
    <main>
      <h1>{snippet.title}</h1>
      <pre>
        <code>{snippet.body}</code>
      </pre>
    </main>
  );
}

Ce return new Response(...) à l'intérieur d'un composant de page n'est pas une astuce : un gestionnaire de route peut renvoyer soit un élément React, soit une Response, et RedwoodSDK traite correctement les deux cas. Un vrai code 404, depuis un composant, sans API spéciale.

Étape 5 : composants client et fonctions serveur

Les Server Components ne peuvent utiliser ni état ni gestionnaires d'événements. Pour le formulaire de création, il vous faut un îlot client, marqué par la directive 'use client' :

// src/app/components/SnippetForm.tsx
"use client";
 
import { useState, useTransition } from "react";
import { saveSnippet } from "@/app/actions/saveSnippet";
 
export function SnippetForm() {
  const [error, setError] = useState<string | null>(null);
  const [isPending, startTransition] = useTransition();
 
  function onSubmit(formData: FormData) {
    startTransition(async () => {
      const result = await saveSnippet(formData);
 
      if (result.ok) {
        window.location.href = `/s/${result.key}`;
      } else {
        setError(result.error);
      }
    });
  }
 
  return (
    <form action={onSubmit}>
      <input name="title" placeholder="Title" required />
      <input name="language" placeholder="typescript" defaultValue="text" />
      <textarea name="body" placeholder="Paste your code" required rows={12} />
 
      <button type="submit" disabled={isPending}>
        {isPending ? "Saving…" : "Save snippet"}
      </button>
 
      {error && <p role="alert">{error}</p>}
    </form>
  );
}

Le formulaire appelle saveSnippet, qui réside dans un fichier marqué 'use server'. Cette fonction n'est jamais envoyée au navigateur. React remplace l'import par un stub RPC, et l'appeler déclenche un POST vers votre Worker :

// src/app/actions/saveSnippet.ts
"use server";
 
import { createSnippet } from "@/db";
 
export async function saveSnippet(formData: FormData) {
  const title = String(formData.get("title") ?? "").trim();
  const body = String(formData.get("body") ?? "");
  const language = String(formData.get("language") ?? "text").trim();
 
  if (title.length === 0) {
    return { ok: false as const, error: "Title is required." };
  }
 
  if (body.length > 100_000) {
    return { ok: false as const, error: "Snippet is too large." };
  }
 
  const key = crypto.randomUUID().slice(0, 8);
  await createSnippet({ key, title, language, body });
 
  return { ok: true as const, key };
}

Deux points méritent d'être soulignés. D'abord, validez à l'intérieur de la fonction serveur, et pas seulement dans le formulaire. La frontière 'use server' est un point d'entrée HTTP public ; n'importe qui peut l'appeler avec n'importe quelle charge utile. La validation côté client est un confort pour l'utilisateur, jamais un contrôle de sécurité.

Ensuite, si une fonction serveur a besoin du contexte de la requête — l'utilisateur courant, un cookie, l'URL — elle importe requestInfo au lieu de le recevoir en props :

"use server";
 
import { requestInfo } from "rwsdk/worker";
 
export async function whoAmI() {
  const { ctx, request } = requestInfo;
  return ctx.user?.username ?? "anonymous";
}

Enfin, affichez l'îlot depuis une page Server Component :

// src/app/pages/NewSnippet.tsx
import { SnippetForm } from "@/app/components/SnippetForm";
 
export function NewSnippet() {
  return (
    <main>
      <h1>New snippet</h1>
      <SnippetForm />
    </main>
  );
}

Seuls SnippetForm et ses dépendances partent vers le navigateur. La coquille de page, la mise en page et le code de votre base de données restent sur le serveur.

Étape 6 : middlewares, ctx et interrupteurs

RedwoodSDK n'a pas de fichier de middleware distinct. Un middleware est simplement une fonction placée plus haut dans le tableau defineApp. Elle reçoit les mêmes informations de requête et peut faire deux choses : modifier ctx sans rien renvoyer (la requête poursuit son chemin), ou renvoyer une Response (tout ce qui suit est court-circuité).

// src/worker.tsx
import { defineApp, ErrorResponse } from "rwsdk/worker";
import { route, render } from "rwsdk/router";
 
export default defineApp([
  // Exécuté à chaque requête, avant la résolution des routes
  async function sessionMiddleware({ request, ctx }) {
    const session = await sessionStore.load(request);
    ctx.session = session ?? { userId: null };
  },
 
  async function userMiddleware({ ctx }) {
    if (ctx.session.userId) {
      ctx.user = await getUser(ctx.session.userId);
    }
  },
 
  render(Document, [
    route("/", Home),
    route("/new", NewSnippet),
    route("/s/:key", SnippetPage),
 
    // Interrupteurs au niveau route : un tableau de gestionnaires, dans l'ordre
    route("/admin", [
      function requireUser({ ctx }) {
        if (!ctx.user) {
          throw new ErrorResponse(401, "Unauthorized");
        }
      },
      AdminPage,
    ]),
  ]),
]);

La route /admin illustre le motif des interrupteurs de RedwoodSDK. Au lieu d'un gestionnaire unique, passez un tableau. Chaque fonction s'exécute dans l'ordre ; la première qui renvoie une Response (ou lève une ErrorResponse) interrompt la chaîne. AdminPage ne s'exécute donc que pour un utilisateur authentifié, et peut lire ctx.user en toute confiance.

Cela se compose bien. Un interrupteur étant une simple fonction, requireUser peut être exporté depuis un module partagé et réutilisé sur une dizaine de routes sans aucune étape d'enregistrement auprès du framework.

Pour typer ctx, étendez l'interface AppContext du framework :

// src/types.d.ts
declare module "rwsdk/worker" {
  interface AppContext {
    session: { userId: string | null };
    user?: { id: string; username: string };
  }
}

Étape 7 : des sessions adossées à un Durable Object

Les cookies seuls ne peuvent pas conserver l'état de session côté serveur, et les Workers n'ont aucune mémoire entre deux requêtes. La réponse de RedwoodSDK est le Durable Object : un objet mono-thread, fortement cohérent, doté de son propre stockage et adressé par identifiant.

Définissez l'objet :

// src/sessions/UserSession.ts
interface SessionData {
  userId: string | null;
}
 
export class UserSession implements DurableObject {
  private storage: DurableObjectStorage;
  private session: SessionData | undefined;
 
  constructor(state: DurableObjectState) {
    this.storage = state.storage;
  }
 
  async getSession() {
    if (!this.session) {
      this.session =
        (await this.storage.get<SessionData>("session")) ?? { userId: null };
    }
    return { value: this.session };
  }
 
  async saveSession(data: Partial<SessionData>) {
    this.session = { userId: data.userId ?? null };
    await this.storage.put("session", this.session);
    return this.session;
  }
 
  async revokeSession() {
    await this.storage.delete("session");
    this.session = undefined;
  }
}

Déclarez le binding dans wrangler.jsonc. Le bloc migrations indique à Cloudflare que cette classe a besoin d'un stockage adossé à SQLite :

{
  "durable_objects": {
    "bindings": [
      { "name": "USER_SESSION_DO", "class_name": "UserSession" }
    ]
  },
  "migrations": [
    { "tag": "v1", "new_sqlite_classes": ["UserSession"] }
  ]
}

Créez ensuite le magasin de sessions dans votre worker et réexportez la classe pour que Wrangler la trouve :

// src/worker.tsx
import { defineDurableSession } from "rwsdk/auth";
import { env } from "cloudflare:workers";
import { UserSession } from "./sessions/UserSession";
 
export const sessionStore = defineDurableSession({
  sessionDurableObject: env.USER_SESSION_DO,
});
 
export { UserSession };

sessionStore.load(request) lit le cookie de session signé, achemine la requête vers la bonne instance de Durable Object et renvoie ses données. C'est exactement le sessionMiddleware de l'étape précédente, désormais entièrement câblé.

Pour une authentification réelle, RedwoodSDK propose un addon passkey qui superpose WebAuthn à ce même magasin de sessions. Comme il est bâti sur les primitives que vous venez de voir, vous pouvez en lire le code source et le comprendre de bout en bout.

Étape 8 : déployer sur l'edge

Le déploiement tient en une commande :

pnpm release

Elle construit les bundles client et serveur, téléverse les ressources statiques, applique la configuration du Worker et publie sur votre sous-domaine workers.dev (ou votre domaine personnalisé, s'il est configuré). Pensez à exécuter d'abord les migrations distantes — une base D1 de production fraîchement créée ne contient aucune table :

npx wrangler d1 migrations apply snippets-db --remote
pnpm release

Pour un environnement de préproduction, définissez un bloc env.staging dans wrangler.jsonc et sélectionnez-le via une variable d'environnement :

CLOUDFLARE_ENV=staging pnpm release

Tester votre implémentation

Vérifiez l'application de bout en bout avant de la considérer terminée :

  1. Lancez pnpm dev et confirmez que la page d'accueil affiche une liste vide sans erreur.
  2. Visitez /new, soumettez un snippet, et vérifiez que le navigateur redirige vers /s/:key avec votre contenu.
  3. Confirmez que la fonction serveur est un vrai point d'entrée. Ouvrez l'onglet réseau du navigateur pendant la soumission. Vous devez voir une requête POST vers la page courante : c'est l'appel RPC. Les fonctions serveur sont des endpoints HTTP, et voir cette requête rend la frontière tangible.
  4. Vérifiez ce qui part vers le client. Affichez le code source de /s/:key. Ni le code de votre base de données ni saveSnippet ne doivent apparaître dans le bundle JavaScript.
  5. Testez le garde-fou. Visitez /admin sans session. Vous devez obtenir un 401, pas une page rendue.
  6. Vérifiez la persistance. Redémarrez pnpm dev et confirmez que vos snippets survivent, puisqu'ils vivent dans le fichier D1 local et non en mémoire.

Une requête rapide confirme que les données ont bien atterri là où vous le pensez :

npx wrangler d1 execute snippets-db --local \
  --command "SELECT key, title FROM snippets"

Dépannage

getRequestInfo() lève une erreur dans un composant de page. Les Server Components reçoivent requestInfo en props. Déstructurez plutôt request, params et ctx depuis les props du composant. Réservez getRequestInfo() et l'import de requestInfo aux fonctions 'use server'.

env.DB is undefined. Le nom du binding dans wrangler.jsonc doit correspondre exactement à la propriété que vous lisez. Un binding nommé DB donne env.DB, jamais env.db. Redémarrez le serveur de développement après avoir modifié wrangler.jsonc, car les bindings sont lus au démarrage.

no such table: snippets en développement. Vous avez appliqué les migrations à distance mais pas en local. Relancez la commande de migration avec le drapeau --local.

Un composant client échoue avec « cannot read state of undefined ». La directive 'use client' doit être la toute première ligne du fichier, avant le moindre import. Une directive mal placée transforme silencieusement le fichier en Server Component.

L'hydratation échoue, ou rien n'est interactif. Vérifiez que votre Document contient toujours la balise script qui importe /src/client.tsx, et que src/client.tsx appelle bien initClient().

Classe Durable Object introuvable au déploiement. La classe doit être exportée depuis le point d'entrée du Worker, et elle nécessite une entrée dans le tableau migrations de wrangler.jsonc. Les deux sont obligatoires.

Pour aller plus loin

Vous disposez désormais d'une application React full-stack qui tourne entièrement sur le réseau Cloudflare, avec base de données, sessions, fonctions serveur et gardes de routes. Quelques pistes valent le détour :

  • Ajoutez le temps réel. RedwoodSDK expose une couche temps réel bâtie sur les Durable Objects et les WebSockets : un nouveau snippet peut apparaître dans tous les onglets ouverts sans interrogation périodique.
  • Diffusez les pages longues. Comme les réponses sont streamées, vous pouvez envelopper une section lente dans un Suspense React et laisser la coquille s'afficher immédiatement.
  • Ajoutez un ORM. Prisma comme Drizzle fonctionnent au-dessus de D1 lorsque vous voulez des migrations et des types générés depuis un schéma plutôt que du SQL écrit à la main.
  • Stockez des fichiers dans R2. Ajoutez un binding R2 et laissez les utilisateurs joindre des images aux snippets. Le motif est identique à celui de D1 : déclarez le binding, accédez-y via env, appelez-le depuis une fonction serveur.

Pour comparer les approches, nos tutoriels sur Waku et OpenNext sur Cloudflare Workers présentent deux visions très différentes du même modèle React sous-jacent. Le guide Cloudflare Workers et Hono est un bon complément si vous souhaitez bâtir une API pure aux côtés de cette application.

Conclusion

L'argument central de RedwoodSDK est qu'un framework doit rendre la plateforme lisible plutôt que la masquer. Tout ce tutoriel en découle : les routes sont des fonctions, les middlewares sont des fonctions, le document HTML vous appartient, et la base de données est atteinte via le même binding env qu'utiliserait un Worker nu.

Le compromis est explicite. Vous renoncez à la portabilité et à un vaste écosystème de plugins. En échange, vous obtenez une base de code où rien d'important ne se produit hors champ — où vous pouvez suivre une requête depuis defineApp, à travers les middlewares, jusque dans un Server Component, ressortir par une fonction serveur et descendre jusqu'à une requête préparée D1, en ne lisant que votre propre code sur tout le trajet.

Pour les équipes déjà engagées sur Cloudflare, cette transparence vaut cher. Partez de l'application de snippets que vous venez de construire, ajoutez le temps réel ou R2 quand le besoin se présente, et laissez le framework s'effacer.