Initial commit
This commit is contained in:
173
skills/creating-client-singletons/SKILL.md
Normal file
173
skills/creating-client-singletons/SKILL.md
Normal 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
|
||||
310
skills/creating-client-singletons/references/common-scenarios.md
Normal file
310
skills/creating-client-singletons/references/common-scenarios.md
Normal 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.
|
||||
@@ -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
|
||||
259
skills/creating-client-singletons/references/test-pattern.md
Normal file
259
skills/creating-client-singletons/references/test-pattern.md
Normal 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
|
||||
Reference in New Issue
Block a user