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,173 @@
---
name: creating-client-singletons
description: Prevent multiple PrismaClient instances that exhaust connection pools causing P1017 errors. Use when creating PrismaClient, exporting database clients, setting up Prisma in new files, or encountering connection pool errors. Critical for serverless environments.
allowed-tools: Read, Write, Edit, Glob, Grep, Bash
version: 1.0.0
---
# PrismaClient Singleton Pattern
Teaches global singleton pattern to prevent multiple PrismaClient instances from exhausting database connection pools.
---
**Role:** Teach proper PrismaClient instantiation and export via global singleton pattern to prevent connection pool exhaustion, P1017 errors, and serverless deployment failures.
**When to Activate:** Creating PrismaClient instances, setting up database clients/exports, encountering P1017 errors, working with serverless environments (Next.js, Lambda, Vercel), or reviewing @prisma/client code.
---
## Problem & Solution
**Problem:** Creating multiple `new PrismaClient()` instances (the #1 Prisma violation) creates separate connection pools causing: pool exhaustion/P1017 errors, performance degradation, serverless failures (Lambda instances × pool size = disaster), and memory waste. Critical: 80% of AI agents in testing created multiple instances causing production failures.
**Solution:** Use global singleton pattern with module-level export: (1) check if PrismaClient exists globally, (2) create if none exists, (3) export for module reuse, (4) never instantiate in functions/classes. Supports module-level singletons (Node.js), global singletons (serverless/hot-reload), test patterns, and pool configuration.
---
## Implementation Workflow
**Phase 1 — Assess:** Grep for `@prisma/client` imports and `new PrismaClient()` calls; identify environment type (hot-reload vs. serverless vs. traditional Node.js vs. tests).
**Phase 2 — Implement:** Choose pattern based on environment; create/update client export (`lib/db.ts` or `lib/prisma.ts`) using global singleton check; update all imports to use singleton; remove duplicate instantiations.
**Phase 3 — Validate:** Grep for `new PrismaClient()` (
should appear once only); test hot reload; verify no P1017 errors; check database connection count; monitor production deployment and logs.
---
## Examples
### Module-Level Singleton (Traditional Node.js)
**File: `lib/db.ts`**
```typescript
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
export default prisma;
```
**Usage:** `import prisma from '@/lib/db'` — works because module loads once in Node.js, creating single shared instance.
---
### Global Singleton (Next.js/Hot Reload)
**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();
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma;
```
**Why:** `globalThis` survives hot module reload; development reuses client across reloads; production creates clean instance per deployment; prevents "too many clients" during development.
---
### Anti-Pattern: Function-Scoped Creation
**WRONG:**
```typescript
async function getUsers() {
const prisma = new PrismaClient(); // ❌ New pool every call
const users = await prisma.user.findMany();
await prisma.$disconnect();
return users;
}
```
**Problems:** New connection pool per function call; connection overhead kills performance; pool never warms up; exhausts connections under load.
**Fix:** `import prisma from '@/lib/db'` and use it directly without creating new instances.
---
## Reference Files
- **Serverless Pattern:** `references/serverless-pattern.md` — Next.js App Router, Vercel, AWS Lambda configurations
- **Test Pattern:** `references/test-pattern.md` — test setup, mocking, isolation strategies
- **Common Scenarios:** `references/common-scenarios.md` — codebase conversion, P1017 troubleshooting, configuration
Load when working with serverless, writing tests, or troubleshooting specific issues.
---
## Constraints
**MUST:** Create PrismaClient exactly
once; export from centralized module (`lib/db.ts`); use global singleton in hot-reload environments; import singleton in all database-access files; never instantiate inside functions or classes.
**SHOULD:** Place in `lib/db.ts`, `lib/prisma.ts`, or `src/db.ts`; configure logging based on NODE_ENV; set connection pool size for deployment; use TypeScript; document connection configuration.
**NEVER:** Create PrismaClient in route handlers, API endpoints, service functions, test files, utility functions; create multiple instances "just to be safe"; disconnect/reconnect repeatedly.
---
## Validation
After implementing:
| Check | Command/Method | Expected | Issue |
| ------------------ | ---------------------------------------------------------------- | -------------------------------------------------------------------------------- | ---------------------------------------- |
| Multiple instances | `grep -r "new PrismaClient()" --include="*.ts" --include="*.js"` | Exactly one occurrence (singleton file only) | Consolidate to single singleton |
| Import patterns | `grep -r "from '@prisma/client'"` | Most imports from singleton module; only singleton imports from `@prisma/client` | Update imports to use singleton |
| Connection pool | Monitor during development hot reload | Connection count stays constant (not growing) | Global singleton pattern not working |
| Production errors | Check logs for P1017 | Zero connection pool errors | Check serverless connection_limit config |
| Test isolation | Run test suite | Tests pass; no connection errors | Ensure tests import singleton |
---
## Standard Client Export
**TypeScript:**
```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;
```
**JavaScript:**
```javascript
const { PrismaClient } = require('@prisma/client');
const globalForPrisma = globalThis;
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;
module.exports = prisma;
```
---
## Quick Reference
**Setup Checklist:**
- [ ] Create `lib/db.ts` or `lib/prisma.ts`; use global singleton pattern (hot reload environments); export single instance; configure logging by NODE_ENV; set connection_limit for serverless; import singleton in all files; never create PrismaClient elsewhere; validate with grep (one instance); test hot reload; monitor production connections
**Red Flags (Implement Singleton Immediately):**
- Multiple `new PrismaClient()` in grep results; P1017 errors in logs; growing connection count during development; different files importing from `@prisma/client`; PrismaClient creation inside functions; test files creating own clients
## Related Skills
**TypeScript Type Safety:**
- If using type guards for singleton validation, use the using-type-guards skill from typescript for type narrowing patterns

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