JB logo

Command Palette

Search for a command to run...

yOUTUBE
Blog
PreviousNext

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.

  1. 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
    
  2. Navigate to the web directory:

    cd web
  3. Create a Contact Form: Open src/app/page.tsx and 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>
      );
    }
  4. Run the frontend:

    pnpm dev
    

    Open http://localhost:3000 and 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.

  1. 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
  2. Navigate to the api directory:

    cd api
  3. Configure Port 8000: Open package.json and update the dev script to run on port 8000.

    "scripts": {
      "dev": "wrangler dev --port 8000",
      // ... other scripts
    }
  4. Create a POST Endpoint: Open src/index.ts and 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;
  5. Run the backend:

    pnpm dev
    

    The 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.

  1. Update src/app/page.tsx in the web directory:

    // 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
    }
  2. 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.

  1. Install Prisma and Postgres driver in api:

    pnpm add prisma --save-dev
    npm install @prisma/client @prisma/extension-accelerate
    
  2. Initialize Prisma:

    pnpm dlx prisma init
    
  3. 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.
  4. Define Schema: Open prisma/schema.prisma and 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())
    }
  5. Push to Database:

    pnpm dlx prisma db push
    
  6. Generate Client:

    pnpm dlx prisma generate
    
  7. Update Hono Handler: Update src/index.ts to 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.

  1. Install Zod:

    pnpm add zod @hono/zod-validator
    
  2. 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.

  1. Install Dependencies:

    pnpm add @scalar/hono-api-reference @hono/zod-openapi
    
  2. Update App to use OpenAPI: We need to switch from standard Hono to OpenAPIHono and define our routes using createRoute.

    // 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;
  3. View Docs: Run the server and visit http://localhost:8000/reference. You will see interactive API documentation generated by Scalar!