Initial commit

This commit is contained in:
Zhongwei Li
2025-11-29 18:22:25 +08:00
commit c3294f28aa
60 changed files with 10297 additions and 0 deletions

View File

@@ -0,0 +1,310 @@
# Common Scenarios
Real-world scenarios and solutions for PrismaClient singleton pattern.
## Scenario 1: Converting Existing Codebase
**Current state:** Multiple files create their own PrismaClient
**Steps:**
1. Create central singleton: `lib/db.ts`
```typescript
import { PrismaClient } from '@prisma/client'
const globalForPrisma = globalThis as unknown as {
prisma: PrismaClient | undefined
}
export const prisma = globalForPrisma.prisma ?? new PrismaClient()
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma
```
2. Use Grep to find all `new PrismaClient()` calls:
```bash
grep -rn "new PrismaClient()" --include="*.ts" --include="*.js" .
```
3. Replace with imports from `lib/db.ts`:
**Before:**
```typescript
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
export async function getUsers() {
return await prisma.user.findMany()
}
```
**After:**
```typescript
import { prisma } from '@/lib/db'
export async function getUsers() {
return await prisma.user.findMany()
}
```
4. Remove old instantiations
5. Validate with grep (should find only one instance):
```bash
grep -rn "new PrismaClient()" --include="*.ts" --include="*.js" . | wc -l
```
Expected: `1`
---
## Scenario 2: Next.js Application
**Setup:**
1. Create `lib/prisma.ts` with global singleton pattern
2. Import in Server Components:
```typescript
import { prisma } from '@/lib/prisma'
export default async function UsersPage() {
const users = await prisma.user.findMany()
return <UserList users={users} />
}
```
3. Import in Server Actions:
```typescript
'use server'
import { prisma } from '@/lib/prisma'
export async function createUser(formData: FormData) {
const email = formData.get('email') as string
return await prisma.user.create({ data: { email } })
}
```
4. Import in Route Handlers:
```typescript
import { NextResponse } from 'next/server'
import { prisma } from '@/lib/prisma'
export async function GET() {
const users = await prisma.user.findMany()
return NextResponse.json(users)
}
```
5. Set `connection_limit=1` in DATABASE_URL for Vercel:
```
DATABASE_URL="postgresql://user:pass@host:5432/db?connection_limit=1"
```
**Validation:**
- Hot reload shouldn't create new connections
- No P1017 errors in development
- Production deployments handle concurrent requests
---
## Scenario 3: Encountering P1017 Errors
**Symptoms:**
- "Can't reach database server" errors
- "Too many connections" in database logs
- Intermittent connection failures
- Error code: P1017
**Diagnosis:**
1. Grep codebase for `new PrismaClient()`:
```bash
grep -rn "new PrismaClient()" --include="*.ts" --include="*.js" .
```
2. Check count of instances:
```bash
grep -rn "new PrismaClient()" --include="*.ts" --include="*.js" . | wc -l
```
If > 1: Multiple instance problem
3. Review connection pool configuration:
```bash
grep -rn "connection_limit" .env* schema.prisma
```
If missing in serverless: Misconfiguration problem
**Fix:**
1. Implement singleton pattern (see Scenario 1)
2. Configure connection_limit for serverless:
**Development (.env.local):**
```
DATABASE_URL="postgresql://user:pass@host:5432/db?connection_limit=10"
```
**Production (Vercel):**
```
DATABASE_URL="postgresql://user:pass@host:5432/db?connection_limit=1"
```
3. Monitor connection count after deployment:
```sql
SELECT count(*) FROM pg_stat_activity WHERE datname = 'your_database';
```
Expected: Should stabilize at reasonable number (not growing)
---
## Scenario 4: Multiple Files Creating Clients
**Problem:** Different service files create their own clients
**Before:**
**`services/users.ts`:**
```typescript
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
export async function getUsers() {
return await prisma.user.findMany()
}
```
**`services/posts.ts`:**
```typescript
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
export async function getPosts() {
return await prisma.post.findMany()
}
```
**Problems:**
- Two separate connection pools
- Doubled memory usage
- Doubled connection count
- Multiplies with every service file
**After:**
**`lib/db.ts`:**
```typescript
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
export default prisma
```
**`services/users.ts`:**
```typescript
import prisma from '@/lib/db'
export async function getUsers() {
return await prisma.user.findMany()
}
```
**`services/posts.ts`:**
```typescript
import prisma from '@/lib/db'
export async function getPosts() {
return await prisma.post.findMany()
}
```
**Result:**
- Single connection pool shared across services
- Reduced memory usage
- Stable connection count
---
## Connection Pool Configuration
The singleton pattern works with proper pool configuration:
**Default pool size:** 10 connections per PrismaClient
**Serverless (Vercel, Lambda):**
```
DATABASE_URL="postgresql://user:pass@host/db?connection_limit=1"
```
**Traditional servers:**
Calculate: `connection_limit = (num_instances * 2) + 1`
- 1 server = 3 connections
- 2 servers = 5 connections
- 4 servers = 9 connections
**Development:**
Default (10) is fine since only one developer instance runs.
**Example configuration per environment:**
```typescript
const connectionLimit = process.env.NODE_ENV === 'production'
? 1
: 10
export const prisma = new PrismaClient({
datasources: {
db: {
url: process.env.DATABASE_URL + `?connection_limit=${connectionLimit}`
}
}
})
```
---
## Why This Matters
Real-world impact from stress testing:
- **80% of agents** created multiple instances
- **100% of those** would fail in production under load
- **P1017 errors** in serverless after ~10 concurrent requests
- **Memory leaks** from abandoned connection pools
- **Database locked out** teams during testing
**The singleton pattern prevents all of these issues.**
Use this pattern **always**, even if your app is small. It becomes critical as you scale, and retrofitting is painful.

View File

@@ -0,0 +1,238 @@
# Serverless Pattern
Serverless environments require special handling due to cold starts, connection pooling, and function lifecycle constraints.
## Next.js App Router (Vercel)
**File: `lib/prisma.ts`**
```typescript
import { PrismaClient } from '@prisma/client'
const globalForPrisma = globalThis as unknown as {
prisma: PrismaClient | undefined
}
export const prisma = globalForPrisma.prisma ?? new PrismaClient({
log: process.env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error'],
})
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma
```
**Environment Configuration (.env):**
```
DATABASE_URL="postgresql://user:pass@host:5432/db?connection_limit=1&pool_timeout=10"
```
**Why connection_limit=1:**
- Each serverless function instance gets ONE connection
- Multiple function instances = multiple connections
- Prevents pool exhaustion with many concurrent requests
- Vercel scales to hundreds of instances automatically
**Usage in Server Components:**
```typescript
import { prisma } from '@/lib/prisma'
export default async function UsersPage() {
const users = await prisma.user.findMany()
return <UserList users={users} />
}
```
**Usage in Server Actions:**
```typescript
'use server'
import { prisma } from '@/lib/prisma'
export async function createUser(formData: FormData) {
const email = formData.get('email') as string
return await prisma.user.create({
data: { email }
})
}
```
**Usage in Route Handlers:**
```typescript
import { NextResponse } from 'next/server'
import { prisma } from '@/lib/prisma'
export async function GET() {
const users = await prisma.user.findMany()
return NextResponse.json(users)
}
```
**Key Points:**
- Never create PrismaClient in component files
- Import singleton from `lib/prisma.ts`
- Global pattern survives hot reload
- Connection limit prevents pool exhaustion
---
## AWS Lambda
**File: `lib/db.ts`**
```typescript
import { PrismaClient } from '@prisma/client'
let prisma: PrismaClient
if (!global.prisma) {
global.prisma = new PrismaClient({
log: ['error', 'warn']
})
}
prisma = global.prisma
export default prisma
```
**Lambda Handler:**
```typescript
import prisma from './lib/db'
export async function handler(event: any) {
const users = await prisma.user.findMany()
return {
statusCode: 200,
body: JSON.stringify(users)
}
}
```
**Environment Variables (Lambda):**
```
DATABASE_URL=postgresql://user:pass@host:5432/db?connection_limit=1&pool_timeout=10&connect_timeout=10
```
**Lambda-Specific Considerations:**
- Lambda reuses container for warm starts
- Global singleton survives across invocations
- First invocation creates client (cold start)
- Subsequent invocations reuse client (warm starts)
- No need to disconnect (Lambda freezes container)
---
## Connection Pool Calculation for Serverless
**Formula:**
```
max_connections = (max_concurrent_functions * connection_limit) + buffer
```
**Example (Vercel):**
- Max concurrent functions: 100
- Connection limit per function: 1
- Buffer: 10
**Result:** Need 110 database connections
**Recommended DATABASE_URL for Vercel:**
```
postgresql://user:pass@host:5432/db?connection_limit=1&pool_timeout=10
```
**Why pool_timeout=10:**
- Prevents long waits for connections
- Fails fast if pool exhausted
- User gets error instead of timeout
---
## Anti-Pattern: Multiple Files Creating Clients
**WRONG - Each file creates its own:**
**`app/api/users/route.ts`:**
```typescript
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
export async function GET() {
return Response.json(await prisma.user.findMany())
}
```
**`app/api/posts/route.ts`:**
```typescript
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
export async function GET() {
return Response.json(await prisma.post.findMany())
}
```
**Problems in Serverless:**
- Each route = separate client = separate pool
- 2 routes × 50 function instances × 10 connections = 1000 connections!
- Database exhausted under load
- P1017 errors inevitable
**Fix - Central singleton:**
**`lib/prisma.ts`:**
```typescript
import { PrismaClient } from '@prisma/client'
const globalForPrisma = globalThis as unknown as {
prisma: PrismaClient | undefined
}
export const prisma = globalForPrisma.prisma ?? new PrismaClient()
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma
```
**`app/api/users/route.ts`:**
```typescript
import { prisma } from '@/lib/prisma'
export async function GET() {
return Response.json(await prisma.user.findMany())
}
```
**`app/api/posts/route.ts`:**
```typescript
import { prisma } from '@/lib/prisma'
export async function GET() {
return Response.json(await prisma.post.findMany())
}
```
**Result:**
- 50 function instances × 1 connection = 50 connections
- Sustainable and scalable
- No P1017 errors

View File

@@ -0,0 +1,259 @@
# Test Pattern
Proper test setup with PrismaClient singleton ensures test isolation and prevents connection exhaustion.
## Test File Setup
**Import singleton, don't create:**
```typescript
import { prisma } from '@/lib/prisma'
describe('User operations', () => {
beforeEach(async () => {
await prisma.user.deleteMany()
})
it('creates user', async () => {
const user = await prisma.user.create({
data: { email: 'test@example.com' }
})
expect(user.email).toBe('test@example.com')
})
afterAll(async () => {
await prisma.$disconnect()
})
})
```
**Key Points:**
- Import singleton, don't create
- Clean state with `deleteMany` or transactions
- Disconnect once at end of suite
- Don't disconnect between tests (kills connection pool)
---
## Test Isolation with Transactions
**Better approach for test isolation:**
```typescript
import { prisma } from '@/lib/prisma'
import { PrismaClient } from '@prisma/client'
describe('User operations', () => {
let testPrisma: Omit<PrismaClient, '$connect' | '$disconnect' | '$on' | '$transaction' | '$use'>
beforeEach(async () => {
await prisma.$transaction(async (tx) => {
testPrisma = tx
await tx.user.deleteMany()
})
})
it('creates user', async () => {
const user = await testPrisma.user.create({
data: { email: 'test@example.com' }
})
expect(user.email).toBe('test@example.com')
})
})
```
**Why this works:**
- Each test runs in transaction
- Automatic rollback after test
- No data leakage between tests
- Faster than deleteMany
---
## Mocking PrismaClient for Unit Tests
**When to mock:**
- Testing business logic without database
- Fast unit tests
- CI/CD pipeline optimization
**File: `__mocks__/prisma.ts`**
```typescript
import { PrismaClient } from '@prisma/client'
import { mockDeep, mockReset, DeepMockProxy } from 'jest-mock-extended'
export const prismaMock = mockDeep<PrismaClient>()
beforeEach(() => {
mockReset(prismaMock)
})
```
**File: `__tests__/userService.test.ts`**
```typescript
import { prismaMock } from '../__mocks__/prisma'
import { createUser } from '../services/userService'
jest.mock('@/lib/prisma', () => ({
__esModule: true,
default: prismaMock,
}))
describe('User Service', () => {
it('creates user with email', async () => {
const mockUser = { id: '1', email: 'test@example.com' }
prismaMock.user.create.mockResolvedValue(mockUser)
const user = await createUser('test@example.com')
expect(user.email).toBe('test@example.com')
expect(prismaMock.user.create).toHaveBeenCalledWith({
data: { email: 'test@example.com' }
})
})
})
```
**Key Points:**
- Mock the singleton module, not PrismaClient
- Reset mocks between tests
- Type-safe mocks with jest-mock-extended
- Fast tests without database
---
## Integration Test Setup
**File: `tests/setup.ts`**
```typescript
import { prisma } from '@/lib/prisma'
beforeAll(async () => {
await prisma.$connect()
})
afterAll(async () => {
await prisma.$disconnect()
})
export async function cleanDatabase() {
const tables = ['User', 'Post', 'Comment']
for (const table of tables) {
await prisma[table.toLowerCase()].deleteMany()
}
}
```
**File: `tests/users.integration.test.ts`**
```typescript
import { prisma } from '@/lib/prisma'
import { cleanDatabase } from './setup'
describe('User Integration Tests', () => {
beforeEach(async () => {
await cleanDatabase()
})
it('creates and retrieves user', async () => {
const created = await prisma.user.create({
data: { email: 'test@example.com' }
})
const retrieved = await prisma.user.findUnique({
where: { id: created.id }
})
expect(retrieved?.email).toBe('test@example.com')
})
it('handles unique constraint', async () => {
await prisma.user.create({
data: { email: 'test@example.com' }
})
await expect(
prisma.user.create({
data: { email: 'test@example.com' }
})
).rejects.toThrow(/Unique constraint/)
})
})
```
**Key Points:**
- Shared setup in `tests/setup.ts`
- Clean database between tests
- Test real database behavior
- Catch constraint violations
---
## Anti-Pattern: Creating Client in Tests
**WRONG:**
```typescript
import { PrismaClient } from '@prisma/client'
describe('User tests', () => {
let prisma: PrismaClient
beforeEach(() => {
prisma = new PrismaClient()
})
afterEach(async () => {
await prisma.$disconnect()
})
it('creates user', async () => {
const user = await prisma.user.create({
data: { email: 'test@example.com' }
})
expect(user.email).toBe('test@example.com')
})
})
```
**Problems:**
- New connection pool every test
- Connect/disconnect overhead
- Connection exhaustion in large suites
- Slow tests
**Fix:**
```typescript
import { prisma } from '@/lib/prisma'
describe('User tests', () => {
beforeEach(async () => {
await prisma.user.deleteMany()
})
it('creates user', async () => {
const user = await prisma.user.create({
data: { email: 'test@example.com' }
})
expect(user.email).toBe('test@example.com')
})
})
```
**Result:**
- Reuses singleton connection
- Fast tests
- No connection exhaustion