Next.js + Hono + Cloudflare + Prisma
This tutorial will guide you through setting up a full-stack application progressively, starting from a simple frontend and backend, and incrementally adding features like database integration, validation, and documentation.
Full Stack Tutorial: Next.js + Hono + Cloudflare + Prisma
This tutorial will guide you through setting up a full-stack application progressively, starting from a simple frontend and backend, and incrementally adding features like database integration, validation, and documentation.
Prerequisites
- Node.js (v18 or later)
- npm or pnpm
- A Cloudflare account (for deploying the backend)
Step 1: Create a Simple Next.js App
First, we'll set up the frontend using Next.js.
-
Initialize the project:
pnpm create next-app@latest web # Select the following options: # - TypeScript: Yes # - ESLint: Yes # - Tailwind CSS: Yes # - `src/` directory: Yes # - App Router: Yes # - Customize import alias: No -
Navigate to the web directory:
cd web -
Create a Contact Form: Open
src/app/page.tsxand replace the content with a simple form.// src/app/page.tsx "use client"; import { useState } from "react"; export default function Home() { const [name, setName] = useState(""); const [phone, setPhone] = useState(""); const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); const data = { name, phone }; console.log("Form Data:", data); alert(JSON.stringify(data, null, 2)); }; return ( <div className="flex min-h-screen flex-col items-center justify-center p-24"> <h1 className="mb-8 text-4xl font-bold">Add Contact</h1> <form onSubmit={handleSubmit} className="flex w-full max-w-md flex-col gap-4" > <input type="text" placeholder="Name" value={name} onChange={(e) => setName(e.target.value)} className="rounded border p-2 text-black" required /> <input type="tel" placeholder="Phone" value={phone} onChange={(e) => setPhone(e.target.value)} className="rounded border p-2 text-black" required /> <button type="submit" className="rounded bg-blue-500 p-2 text-white hover:bg-blue-600" > Submit </button> </form> </div> ); } -
Run the frontend:
pnpm devOpen
http://localhost:3000and test the form. You should see the data logged in your browser console.
Step 2: Create a Simple Hono App (Backend)
Now, let's set up the backend with Hono and Cloudflare Workers.
-
Initialize the project: Go back to the root directory and create the api project.
cd .. npm create hono@latest api # Select the following options: # - Template: cloudflare-workers # - Install dependencies: Yes -
Navigate to the api directory:
cd api -
Configure Port 8000: Open
package.jsonand update thedevscript to run on port 8000."scripts": { "dev": "wrangler dev --port 8000", // ... other scripts } -
Create a POST Endpoint: Open
src/index.tsand set up the server.// src/index.ts import { Hono } from "hono"; import { cors } from "hono/cors"; const app = new Hono(); // Enable CORS so our frontend can talk to the backend app.use("/*", cors()); app.get("/", (c) => { return c.text("Hello Hono!"); }); app.post("/contacts", async (c) => { const body = await c.req.json(); console.log("Received data:", body); return c.json({ message: "Data received", data: body }); }); export default app; -
Run the backend:
pnpm devThe server is now listening on
http://localhost:8000.
Step 3: Connect Frontend to Backend
Now we will update the frontend to send data to our new Hono API.
-
Update
src/app/page.tsxin thewebdirectory:// src/app/page.tsx // ... imports export default function Home() { // ... state const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); const data = { name, phone }; try { const response = await fetch("http://localhost:8000/contacts", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify(data), }); const result = await response.json(); console.log("Server Response:", result); alert("Saved! Check server console."); } catch (error) { console.error("Error:", error); alert("Failed to save."); } }; // ... return } -
Test it: Run both frontend and backend. Submit the form on the frontend. You should see "Received data: ..." in the terminal where the Hono app is running.
Step 4: Add Prisma Integration
Let's add a database to store our contacts.
-
Install Prisma and Postgres driver in
api:pnpm add prisma --save-dev npm install @prisma/client @prisma/extension-accelerate -
Initialize Prisma:
pnpm dlx prisma init -
Configure Database URL: In
api/.env, add your PostgreSQL connection string (e.g., from Neon, Supabase, or local).DATABASE_URL="postgresql://user:password@host:5432/db?sslmode=require" DIRECT_URL="postgresql://user:password@host:5432/db?sslmode=require" # Note: For Cloudflare Workers, use the connection pool URL for DATABASE_URL if available, or use Prisma Accelerate. -
Define Schema: Open
prisma/schema.prismaand add the Contact model.// prisma/schema.prisma generator client { provider = "prisma-client-js" } datasource db { provider = "postgresql" url = env("DATABASE_URL") } model Contact { id String @id @default(uuid()) name String phone String createdAt DateTime @default(now()) } -
Push to Database:
pnpm dlx prisma db push -
Generate Client:
pnpm dlx prisma generate -
Update Hono Handler: Update
src/index.tsto save data to the database.// src/index.ts import { Hono } from "hono"; import { cors } from "hono/cors"; import { PrismaClient } from "@prisma/client/edge"; import { withAccelerate } from "@prisma/extension-accelerate"; // Initialize Prisma Client with Accelerate for Edge compatibility const prisma = new PrismaClient().$extends(withAccelerate()); const app = new Hono(); app.use("/*", cors()); app.post("/contacts", async (c) => { const body = await c.req.json(); try { const contact = await prisma.contact.create({ data: { name: body.name, phone: body.phone, }, }); return c.json({ message: "Contact created", contact }); } catch (e) { return c.json({ error: "Failed to create contact" }, 500); } }); export default app;
Step 5: Add Zod Validation
Now we'll ensure the data sent to the API is valid.
-
Install Zod:
pnpm add zod @hono/zod-validator -
Create a Schema and Validator: Update
src/index.ts.// src/index.ts import { z } from "zod"; import { zValidator } from "@hono/zod-validator"; // ... imports const contactSchema = z.object({ name: z.string().min(2), phone: z.string().min(10), }); app.post( "/contacts", zValidator("json", contactSchema), // Middleware validates the body async (c) => { const body = c.req.valid("json"); // Typed body! // ... prisma create logic using 'body' } );
Step 6: Add Scalar Docs (OpenAPI)
Finally, let's document our API.
-
Install Dependencies:
pnpm add @scalar/hono-api-reference @hono/zod-openapi -
Update App to use OpenAPI: We need to switch from standard
HonotoOpenAPIHonoand define our routes usingcreateRoute.// src/index.ts import { OpenAPIHono, createRoute, z } from "@hono/zod-openapi"; import { apiReference } from "@scalar/hono-api-reference"; import { cors } from "hono/cors"; // ... prisma imports const app = new OpenAPIHono(); app.use("/*", cors()); const contactSchema = z.object({ name: z.string().min(2).openapi({ example: "John Doe" }), phone: z.string().min(10).openapi({ example: "1234567890" }), }); const route = createRoute({ method: "post", path: "/contacts", request: { body: { content: { "application/json": { schema: contactSchema, }, }, }, }, responses: { 200: { content: { "application/json": { schema: z.object({ message: z.string(), contact: contactSchema, }), }, }, description: "Contact created successfully", }, }, }); app.openapi(route, async (c) => { const body = c.req.valid("json"); // ... prisma logic return c.json({ message: "Created", contact: body }); // Simplified for example }); // Serve the docs app.doc("/doc", { openapi: "3.0.0", info: { version: "1.0.0", title: "My API", }, }); app.get( "/reference", apiReference({ spec: { url: "/doc", }, }) ); export default app; -
View Docs: Run the server and visit
http://localhost:8000/reference. You will see interactive API documentation generated by Scalar!

