From 9bdc879f5d52d89cc1e1a18291f7030d2b03013d Mon Sep 17 00:00:00 2001 From: JamesbbBriz Date: Sun, 22 Mar 2026 15:24:25 +1000 Subject: [PATCH] feat(examples): add Prisma + Hyperdrive example for Cloudflare Workers Full-stack CRUD example demonstrating: - Fresh PrismaClient per call (guarantees request isolation on Workers) - Hyperdrive connection pooling via cloudflare:workers binding - Server component data fetching with Prisma - Route handler CRUD API using standard Web Request/Response --- examples/prisma-hyperdrive/README.md | 93 +++++++++++++++++++ .../prisma-hyperdrive/app/api/items/route.ts | 35 +++++++ examples/prisma-hyperdrive/app/layout.tsx | 14 +++ examples/prisma-hyperdrive/app/page.tsx | 29 ++++++ examples/prisma-hyperdrive/lib/db.ts | 29 ++++++ examples/prisma-hyperdrive/package.json | 33 +++++++ .../prisma-hyperdrive/prisma/schema.prisma | 17 ++++ examples/prisma-hyperdrive/tsconfig.json | 16 ++++ examples/prisma-hyperdrive/vite.config.ts | 15 +++ examples/prisma-hyperdrive/worker/index.ts | 13 +++ examples/prisma-hyperdrive/wrangler.jsonc | 23 +++++ 11 files changed, 317 insertions(+) create mode 100644 examples/prisma-hyperdrive/README.md create mode 100644 examples/prisma-hyperdrive/app/api/items/route.ts create mode 100644 examples/prisma-hyperdrive/app/layout.tsx create mode 100644 examples/prisma-hyperdrive/app/page.tsx create mode 100644 examples/prisma-hyperdrive/lib/db.ts create mode 100644 examples/prisma-hyperdrive/package.json create mode 100644 examples/prisma-hyperdrive/prisma/schema.prisma create mode 100644 examples/prisma-hyperdrive/tsconfig.json create mode 100644 examples/prisma-hyperdrive/vite.config.ts create mode 100644 examples/prisma-hyperdrive/worker/index.ts create mode 100644 examples/prisma-hyperdrive/wrangler.jsonc diff --git a/examples/prisma-hyperdrive/README.md b/examples/prisma-hyperdrive/README.md new file mode 100644 index 000000000..c9c56d950 --- /dev/null +++ b/examples/prisma-hyperdrive/README.md @@ -0,0 +1,93 @@ +# Prisma + Hyperdrive on Cloudflare Workers + +A full-stack CRUD example using [Prisma](https://www.prisma.io/) with [Hyperdrive](https://developers.cloudflare.com/hyperdrive/) for accelerated PostgreSQL access on Cloudflare Workers via vinext. + +Demonstrates: +- Per-request Prisma client (avoids [alternating connection failures](https://github.com/cloudflare/vinext/issues/537)) +- Hyperdrive connection pooling +- Server component data fetching +- Route handler CRUD API + +## Prerequisites + +- A PostgreSQL database (Neon, Supabase, or any provider) +- A Cloudflare account with Hyperdrive enabled + +## Setup + +1. Install dependencies: + +```bash +pnpm install +``` + +2. Set your database URL: + +```bash +echo 'DATABASE_URL="postgresql://user:pass@host:5432/db"' > .env +``` + +3. Create the database table: + +```bash +pnpm db:push +``` + +4. Generate the Prisma client: + +```bash +pnpm db:generate +``` + +5. Create a Hyperdrive config: + +```bash +npx wrangler hyperdrive create my-db \ + --connection-string="postgresql://user:pass@host:5432/db" +``` + +6. Copy the Hyperdrive ID into `wrangler.jsonc`: + +```jsonc +"hyperdrive": [{ "binding": "HYPERDRIVE", "id": "" }] +``` + +## Development + +```bash +pnpm dev +``` + +Open http://localhost:5173 — the app fetches items from your database via Hyperdrive. + +## Deploy + +```bash +pnpm build +npx wrangler deploy +``` + +## How it works + +### The per-request client pattern + +Cloudflare Workers reuse isolates across requests, but each request has its own I/O context. A global `PrismaClient` singleton causes alternating failures because the connection pool from one request becomes invalid in the next. + +`lib/db.ts` solves this by creating a fresh client per call: + +```ts +export function getPrisma(): PrismaClient { + const pool = new Pool({ connectionString: env.HYPERDRIVE.connectionString }); + return new PrismaClient({ adapter: new PrismaPg(pool) }); +} +``` + +Each call creates a fresh client — no cross-request state leakage. PrismaClient construction is lightweight (~0.1ms); the expensive part is the query, which Hyperdrive accelerates via edge connection pooling. + +### Hyperdrive + +Hyperdrive pools and caches PostgreSQL connections at Cloudflare's edge. The connection string comes from the `HYPERDRIVE` binding in `wrangler.jsonc`, accessed via `import { env } from "cloudflare:workers"`. + +### Route handlers use standard Request + +vinext route handlers receive the Web standard `Request` object (not `NextRequest`). Use `new URL(request.url)` to access URL components like pathname or search params. diff --git a/examples/prisma-hyperdrive/app/api/items/route.ts b/examples/prisma-hyperdrive/app/api/items/route.ts new file mode 100644 index 000000000..a8c282d13 --- /dev/null +++ b/examples/prisma-hyperdrive/app/api/items/route.ts @@ -0,0 +1,35 @@ +import { getPrisma } from "@/lib/db"; + +export async function GET() { + const prisma = getPrisma(); + const items = await prisma.item.findMany({ orderBy: { createdAt: "desc" } }); + return Response.json(items); +} + +export async function POST(request: Request) { + const contentType = request.headers.get("content-type") ?? ""; + + let title: string | null = null; + + if (contentType.includes("application/json")) { + const body = await request.json(); + title = body.title; + } else { + // Form submission + const formData = await request.formData(); + title = formData.get("title") as string; + } + + if (!title?.trim()) { + return Response.json({ error: "title is required" }, { status: 400 }); + } + + const prisma = getPrisma(); + const item = await prisma.item.create({ data: { title: title.trim() } }); + + // Redirect back to home on form submit, return JSON for API calls + if (!contentType.includes("application/json")) { + return new Response(null, { status: 303, headers: { Location: "/" } }); + } + return Response.json(item, { status: 201 }); +} diff --git a/examples/prisma-hyperdrive/app/layout.tsx b/examples/prisma-hyperdrive/app/layout.tsx new file mode 100644 index 000000000..63cfa007d --- /dev/null +++ b/examples/prisma-hyperdrive/app/layout.tsx @@ -0,0 +1,14 @@ +export const metadata = { + title: "Prisma + Hyperdrive on vinext", + description: "Full-stack example with Prisma, Hyperdrive, and Cloudflare Workers", +}; + +export default function RootLayout({ children }: { children: React.ReactNode }) { + return ( + + + {children} + + + ); +} diff --git a/examples/prisma-hyperdrive/app/page.tsx b/examples/prisma-hyperdrive/app/page.tsx new file mode 100644 index 000000000..35b0f3e73 --- /dev/null +++ b/examples/prisma-hyperdrive/app/page.tsx @@ -0,0 +1,29 @@ +import { getPrisma } from "@/lib/db"; + +export default async function Home() { + const prisma = getPrisma(); + const items = await prisma.item.findMany({ orderBy: { createdAt: "desc" } }); + + return ( +
+

Items ({items.length})

+
+ + +
+
    + {items.map((item) => ( +
  • + + {item.title} + + + {item.createdAt.toLocaleDateString()} + +
  • + ))} + {items.length === 0 &&
  • No items yet. Add one above.
  • } +
+
+ ); +} diff --git a/examples/prisma-hyperdrive/lib/db.ts b/examples/prisma-hyperdrive/lib/db.ts new file mode 100644 index 000000000..fac54f2d3 --- /dev/null +++ b/examples/prisma-hyperdrive/lib/db.ts @@ -0,0 +1,29 @@ +/** + * Per-request Prisma client for Cloudflare Workers. + * + * Workers reuse isolates across requests, but each request has its own + * I/O context. A global PrismaClient singleton causes alternating + * connection failures (success, fail, success, fail...) because the + * connection from Request A is invalid in Request B. + * + * Solution: create a fresh PrismaClient for every call. PrismaClient + * construction is lightweight (~0.1ms) — the expensive part is the + * actual query, which Hyperdrive accelerates via connection pooling. + */ +import { PrismaClient } from "@prisma/client"; +import { PrismaPg } from "@prisma/adapter-pg"; +import { Pool } from "pg"; +import { env } from "cloudflare:workers"; + +/** + * Get a fresh PrismaClient for the current request. + * + * Each call creates a new client to guarantee request isolation. + * Hyperdrive handles connection pooling at the edge — no need to + * cache or reuse the client across calls. + */ +export function getPrisma(): PrismaClient { + const pool = new Pool({ connectionString: env.HYPERDRIVE.connectionString }); + const adapter = new PrismaPg(pool); + return new PrismaClient({ adapter }); +} diff --git a/examples/prisma-hyperdrive/package.json b/examples/prisma-hyperdrive/package.json new file mode 100644 index 000000000..048326999 --- /dev/null +++ b/examples/prisma-hyperdrive/package.json @@ -0,0 +1,33 @@ +{ + "name": "vinext-prisma-hyperdrive", + "type": "module", + "private": true, + "scripts": { + "dev": "vp dev", + "build": "vp build", + "preview": "vp preview", + "db:generate": "prisma generate", + "db:migrate": "prisma migrate dev", + "db:push": "prisma db push" + }, + "dependencies": { + "@vitejs/plugin-react": "catalog:", + "react": "catalog:", + "react-dom": "catalog:", + "vite": "catalog:", + "vinext": "workspace:*", + "@vitejs/plugin-rsc": "catalog:", + "react-server-dom-webpack": "catalog:", + "@cloudflare/vite-plugin": "catalog:", + "wrangler": "catalog:", + "@prisma/client": "^7.0.0", + "@prisma/adapter-pg": "^7.0.0", + "pg": "^8.13.0" + }, + "devDependencies": { + "@cloudflare/workers-types": "catalog:", + "vite-plus": "catalog:", + "prisma": "^7.0.0", + "@types/pg": "^8.11.0" + } +} diff --git a/examples/prisma-hyperdrive/prisma/schema.prisma b/examples/prisma-hyperdrive/prisma/schema.prisma new file mode 100644 index 000000000..7d2c6c79d --- /dev/null +++ b/examples/prisma-hyperdrive/prisma/schema.prisma @@ -0,0 +1,17 @@ +generator client { + provider = "prisma-client-js" + previewFeatures = ["driverAdapters"] +} + +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} + +model Item { + id String @id @default(uuid()) + title String + completed Boolean @default(false) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} diff --git a/examples/prisma-hyperdrive/tsconfig.json b/examples/prisma-hyperdrive/tsconfig.json new file mode 100644 index 000000000..02995fb74 --- /dev/null +++ b/examples/prisma-hyperdrive/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "bundler", + "jsx": "react-jsx", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "paths": { + "@/*": ["./*"] + } + }, + "include": ["**/*.ts", "**/*.tsx"], + "exclude": ["node_modules"] +} diff --git a/examples/prisma-hyperdrive/vite.config.ts b/examples/prisma-hyperdrive/vite.config.ts new file mode 100644 index 000000000..947e59894 --- /dev/null +++ b/examples/prisma-hyperdrive/vite.config.ts @@ -0,0 +1,15 @@ +import { defineConfig } from "vite"; +import vinext from "vinext"; +import { cloudflare } from "@cloudflare/vite-plugin"; + +export default defineConfig({ + plugins: [ + vinext(), + cloudflare({ + viteEnvironment: { + name: "rsc", + childEnvironments: ["ssr"], + }, + }), + ], +}); diff --git a/examples/prisma-hyperdrive/worker/index.ts b/examples/prisma-hyperdrive/worker/index.ts new file mode 100644 index 000000000..b0bc76fd6 --- /dev/null +++ b/examples/prisma-hyperdrive/worker/index.ts @@ -0,0 +1,13 @@ +/** + * Cloudflare Worker entry point for vinext App Router. + * + * For apps without image optimization, point wrangler.jsonc main + * directly at "vinext/server/app-router-entry" instead of this file. + */ +import handler from "vinext/server/app-router-entry"; + +export default { + async fetch(request: Request): Promise { + return handler.fetch(request); + }, +}; diff --git a/examples/prisma-hyperdrive/wrangler.jsonc b/examples/prisma-hyperdrive/wrangler.jsonc new file mode 100644 index 000000000..7333c1abd --- /dev/null +++ b/examples/prisma-hyperdrive/wrangler.jsonc @@ -0,0 +1,23 @@ +{ + "$schema": "node_modules/wrangler/config-schema.json", + "name": "prisma-hyperdrive", + "compatibility_date": "2026-03-01", + "compatibility_flags": ["nodejs_compat"], + "main": "./worker/index.ts", + "preview_urls": true, + "assets": { + "not_found_handling": "none", + "binding": "ASSETS" + }, + "images": { + "binding": "IMAGES" + }, + // Hyperdrive accelerates PostgreSQL connections via connection pooling. + // Create one with: wrangler hyperdrive create my-db --connection-string="postgres://..." + "hyperdrive": [ + { + "binding": "HYPERDRIVE", + "id": "" + } + ] +}