Files
gh-jezweb-claude-skills-ski…/references/prisma-integration.md
2025-11-30 08:24:16 +08:00

13 KiB

Prisma ORM Integration Guide

Complete guide to using Prisma ORM with Cloudflare Hyperdrive.


Overview

Prisma ORM is a popular Node.js and TypeScript ORM focused on type safety and developer experience.

Why Prisma + Hyperdrive?

  • Excellent TypeScript support and auto-completion
  • Powerful migrations and schema management
  • Intuitive API with .findMany(), .create(), etc.
  • Works with Hyperdrive via driver adapters

CRITICAL: Prisma requires driver adapters (@prisma/adapter-pg) to work with Hyperdrive.


Installation

# Prisma CLI and client
npm install prisma @prisma/client

# PostgreSQL driver and adapter
npm install pg @prisma/adapter-pg

# TypeScript types for pg
npm install -D @types/pg

Setup

1. Initialize Prisma

npx prisma init

This creates:

  • prisma/ directory
  • prisma/schema.prisma file
  • .env file

2. Configure Schema

Edit prisma/schema.prisma:

generator client {
  provider        = "prisma-client-js"
  previewFeatures = ["driverAdapters"]  // REQUIRED for Hyperdrive
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

model User {
  id        Int      @id @default(autoincrement())
  name      String
  email     String   @unique
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
  posts     Post[]
}

model Post {
  id        Int      @id @default(autoincrement())
  title     String
  content   String?
  published Boolean  @default(false)
  authorId  Int
  author    User     @relation(fields: [authorId], references: [id])
  createdAt DateTime @default(now())
}

3. Set Database URL

Edit .env (for migrations only, NOT used in Worker):

# Direct connection to database (for migrations)
DATABASE_URL="postgres://user:password@host:5432/database"

Important: This .env file is only for running migrations locally. Workers get connection string from Hyperdrive binding.


4. Generate Prisma Client

npx prisma generate --no-engine

CRITICAL: Use --no-engine flag for Workers compatibility.

This generates the Prisma Client in node_modules/@prisma/client.


5. Run Migrations

# Create and apply migration
npx prisma migrate dev --name init

# Or apply existing migrations
npx prisma migrate deploy

Generated SQL (in prisma/migrations/ folder):

-- CreateTable
CREATE TABLE "User" (
    "id" SERIAL NOT NULL,
    "name" TEXT NOT NULL,
    "email" TEXT NOT NULL,
    "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
    "updatedAt" TIMESTAMP(3) NOT NULL,
    PRIMARY KEY ("id")
);

-- CreateTable
CREATE TABLE "Post" (
    "id" SERIAL NOT NULL,
    "title" TEXT NOT NULL,
    "content" TEXT,
    "published" BOOLEAN NOT NULL DEFAULT false,
    "authorId" INTEGER NOT NULL,
    "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
    PRIMARY KEY ("id")
);

-- CreateIndex
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");

-- AddForeignKey
ALTER TABLE "Post" ADD CONSTRAINT "Post_authorId_fkey"
  FOREIGN KEY ("authorId") REFERENCES "User"("id")
  ON DELETE RESTRICT ON UPDATE CASCADE;

Use in Worker

import { PrismaPg } from "@prisma/adapter-pg";
import { PrismaClient } from "@prisma/client";
import { Pool } from "pg";

type Bindings = {
  HYPERDRIVE: Hyperdrive;
};

export default {
  async fetch(request: Request, env: Bindings, ctx: ExecutionContext): Promise<Response> {
    // Create pg.Pool for driver adapter
    const pool = new Pool({
      connectionString: env.HYPERDRIVE.connectionString,
      max: 5  // CRITICAL: Workers limit is 6 concurrent connections
    });

    // Create Prisma driver adapter
    const adapter = new PrismaPg(pool);

    // Create Prisma client with adapter
    const prisma = new PrismaClient({ adapter });

    try {
      // Create user
      const newUser = await prisma.user.create({
        data: {
          name: "John Doe",
          email: `john.${Date.now()}@example.com`
        }
      });

      // Find all users
      const allUsers = await prisma.user.findMany();

      // Find user by email
      const user = await prisma.user.findUnique({
        where: { email: "john@example.com" }
      });

      // Update user
      await prisma.user.update({
        where: { id: newUser.id },
        data: { name: "Jane Doe" }
      });

      // Create post with relation
      await prisma.post.create({
        data: {
          title: "My First Post",
          content: "Hello World!",
          published: true,
          authorId: newUser.id
        }
      });

      // Find users with posts (include relation)
      const usersWithPosts = await prisma.user.findMany({
        include: {
          posts: true
        }
      });

      return Response.json({
        newUser,
        allUsers,
        user,
        usersWithPosts
      });
    } catch (error: any) {
      return Response.json({
        error: error.message
      }, { status: 500 });
    } finally {
      // Clean up pool connections
      ctx.waitUntil(pool.end());
    }
  }
};

Common Query Patterns

Create

// Create single record
const user = await prisma.user.create({
  data: {
    name: "John",
    email: "john@example.com"
  }
});

// Create with relation
const post = await prisma.post.create({
  data: {
    title: "Hello",
    content: "World",
    author: {
      connect: { id: userId }
    }
  }
});

// Create with nested relation
const userWithPost = await prisma.user.create({
  data: {
    name: "John",
    email: "john@example.com",
    posts: {
      create: [
        { title: "First Post", content: "Hello" }
      ]
    }
  }
});

Read

// Find all
const users = await prisma.user.findMany();

// Find with filter
const activeUsers = await prisma.user.findMany({
  where: { active: true }
});

// Find unique
const user = await prisma.user.findUnique({
  where: { email: "john@example.com" }
});

// Find first
const firstUser = await prisma.user.findFirst({
  where: { name: "John" }
});

// Find with relations
const usersWithPosts = await prisma.user.findMany({
  include: {
    posts: true
  }
});

// Pagination
const users = await prisma.user.findMany({
  skip: 20,
  take: 10
});

// Sorting
const users = await prisma.user.findMany({
  orderBy: {
    createdAt: 'desc'
  }
});

Update

// Update one
const user = await prisma.user.update({
  where: { id: 1 },
  data: { name: "Jane" }
});

// Update many
const result = await prisma.user.updateMany({
  where: { active: false },
  data: { deleted: true }
});

// Upsert (update or insert)
const user = await prisma.user.upsert({
  where: { email: "john@example.com" },
  update: { name: "John Updated" },
  create: { name: "John", email: "john@example.com" }
});

Delete

// Delete one
const user = await prisma.user.delete({
  where: { id: 1 }
});

// Delete many
const result = await prisma.user.deleteMany({
  where: { active: false }
});

Aggregations

// Count
const count = await prisma.user.count();

// Count with filter
const activeCount = await prisma.user.count({
  where: { active: true }
});

// Aggregate
const result = await prisma.post.aggregate({
  _count: { id: true },
  _avg: { views: true },
  _sum: { views: true },
  _min: { createdAt: true },
  _max: { createdAt: true }
});

// Group by
const result = await prisma.post.groupBy({
  by: ['authorId'],
  _count: { id: true }
});

Complex Filters

// AND
const users = await prisma.user.findMany({
  where: {
    AND: [
      { active: true },
      { role: 'admin' }
    ]
  }
});

// OR
const users = await prisma.user.findMany({
  where: {
    OR: [
      { role: 'admin' },
      { role: 'moderator' }
    ]
  }
});

// NOT
const users = await prisma.user.findMany({
  where: {
    NOT: {
      email: {
        endsWith: '@example.com'
      }
    }
  }
});

// Comparison operators
const users = await prisma.user.findMany({
  where: {
    createdAt: {
      gte: new Date('2024-01-01'),
      lt: new Date('2024-12-31')
    }
  }
});

// String filters
const users = await prisma.user.findMany({
  where: {
    email: {
      contains: '@gmail.com'  // or startsWith, endsWith
    }
  }
});

Transactions

// Sequential operations (default)
const result = await prisma.$transaction([
  prisma.user.create({ data: { name: "John", email: "john@example.com" } }),
  prisma.post.create({ data: { title: "Hello", authorId: 1 } })
]);

// Interactive transactions
const result = await prisma.$transaction(async (tx) => {
  const user = await tx.user.create({
    data: { name: "John", email: "john@example.com" }
  });

  const post = await tx.post.create({
    data: { title: "Hello", authorId: user.id }
  });

  return { user, post };
});

// Transaction with error handling
try {
  const result = await prisma.$transaction(async (tx) => {
    const user = await tx.user.create({ data: { ... } });

    if (someCondition) {
      throw new Error("Rollback transaction");
    }

    return user;
  });
} catch (error) {
  console.error("Transaction failed:", error);
}

TypeScript Types

Prisma automatically generates TypeScript types:

import { User, Post, Prisma } from "@prisma/client";

// Model types
const user: User = await prisma.user.findUnique({ where: { id: 1 } });

// Input types
const createUserData: Prisma.UserCreateInput = {
  name: "John",
  email: "john@example.com"
};

// Return types with relations
type UserWithPosts = Prisma.UserGetPayload<{
  include: { posts: true }
}>;

const user: UserWithPosts = await prisma.user.findUnique({
  where: { id: 1 },
  include: { posts: true }
});

Prisma Studio

View and edit database with Prisma Studio:

npx prisma studio

Note: Runs locally, connects to database directly (not via Hyperdrive).


Best Practices

  1. Use driver adapters (@prisma/adapter-pg) - REQUIRED for Hyperdrive
  2. Generate with --no-engine - Required for Workers compatibility
  3. Set max: 5 for pg.Pool - Stay within Workers' 6 connection limit
  4. Use ctx.waitUntil() for cleanup
  5. Run migrations outside Worker - Use prisma migrate locally
  6. Version control migrations in prisma/migrations/ folder
  7. Use .env for migrations only - Not used in Worker runtime
  8. Re-generate client after schema changes: npx prisma generate --no-engine

Prisma vs Drizzle

Feature Prisma Drizzle
Type Safety Excellent Excellent
Migrations Prisma Migrate (powerful) Drizzle Kit (simpler)
API Style .findMany(), .create() .select().from() (SQL-like)
Bundle Size ⚠️ Larger Smaller
Workers Setup ⚠️ Needs adapters + --no-engine Simpler setup
Learning Curve ⚠️ Steeper Easier (if you know SQL)
Performance Good Excellent

Recommendation:

  • Use Prisma if you want powerful migrations and intuitive API
  • Use Drizzle if you want lighter bundle size and SQL-like queries

Common Issues

Error: "PrismaClient is unable to run in this browser environment"

Cause: Prisma Client not generated with --no-engine flag.

Solution:

npx prisma generate --no-engine

Error: "Cannot find module '@prisma/client'"

Cause: Prisma Client not generated.

Solution:

npm install @prisma/client
npx prisma generate --no-engine

Error: "Database xxx does not exist"

Cause: DATABASE_URL in .env points to non-existent database.

Solution:

  1. Create database: CREATE DATABASE xxx;
  2. Verify DATABASE_URL in .env

Error: "No such module 'node:*'"

Cause: nodejs_compat flag not enabled.

Solution: Add to wrangler.jsonc:

{
  "compatibility_flags": ["nodejs_compat"]
}

Package Scripts

Add to package.json:

{
  "scripts": {
    "db:generate": "prisma generate --no-engine",
    "db:migrate": "prisma migrate dev",
    "db:deploy": "prisma migrate deploy",
    "db:studio": "prisma studio",
    "db:push": "prisma db push"
  }
}

Usage:

npm run db:generate  # Generate Prisma Client
npm run db:migrate   # Create and apply migration
npm run db:studio    # Open Prisma Studio

References