postgres preset (typical Prisma backend)
Prisma most commonly targets PostgreSQL or MySQL. The 'postgres' preset is the right starting point for both. Use 'mysql' if your schema targets MySQL.
Prisma's model delegates (prisma.user, prisma.post) are not plain objects with async methods — they're Proxy-backed delegate objects. withLatencyAll(prisma, 'postgres') won't traverse the delegate chain. Instead, wrap the individual operations or create thin wrapper functions as shown below.
Wrap individual Prisma operations
The most direct approach: wrap a lambda that calls the Prisma operation. The lambda is a plain async function, so slowdep can wrap it normally.
import { PrismaClient } from '@prisma/client'; import { withLatency } from 'slowdep'; const prisma = new PrismaClient(); // Wrap each operation as a lambda — prisma.user.findUnique is not a standalone function const findUserById = withLatency( (id: string) => prisma.user.findUnique({ where: { id } }), 'postgres' ); const createUser = withLatency( (data: { email: string; name: string }) => prisma.user.create({ data }), 'postgres' ); const findPostsByUser = withLatency( (userId: string) => prisma.post.findMany({ where: { authorId: userId }, orderBy: { createdAt: 'desc' }, take: 20 }), 'postgres' ); // Now use them like normal async functions const user = await findUserById('clx1234abcde'); const posts = await findPostsByUser(user.id);
A helper pattern for wrapping many operations
When you have many operations to wrap, build a factory function to avoid repetition. This keeps your repository layer clean while applying latency consistently.
import { PrismaClient } from '@prisma/client'; import { withLatency } from 'slowdep'; const prisma = new PrismaClient(); // Helper: adds latency to any prisma operation in non-production environments function slow<T extends any[], R>(fn: (...args: T) => Promise<R>) { return process.env.NODE_ENV === 'production' ? fn : withLatency(fn, 'postgres'); } // Build a typed repository with consistent latency export const userRepo = { findById: slow((id: string) => prisma.user.findUnique({ where: { id } })), findByEmail: slow((email: string) => prisma.user.findUnique({ where: { email } })), create: slow((data: { email: string; name: string }) => prisma.user.create({ data })), update: slow((id: string, data: Partial<{ name: string }>) => prisma.user.update({ where: { id }, data })), delete: slow((id: string) => prisma.user.delete({ where: { id } })), findMany: slow((args?: { take?: number; skip?: number }) => prisma.user.findMany(args)), }; // Clean, typed, realistic — use userRepo throughout your service const user = await userRepo.findByEmail('alice@example.com');
Jest tests with Prisma and slowdep
The recommended pattern: mock the Prisma client, then apply slowdep to the mock. This gives you both realistic latency and controlled return values in the same test.
import { withLatency } from 'slowdep'; import { getUserProfile } from '../src/services/userService'; test('getUserProfile returns profile within 500ms', async () => { // Create a mock that returns a known user, with realistic latency const mockFindUnique = withLatency( jest.fn().mockResolvedValue({ id: 'u1', email: 'alice@example.com', name: 'Alice' }), 'postgres' ); // Inject the slow mock into the service const profile = await getUserProfile('u1', { findUser: mockFindUnique }); expect(profile.name).toBe('Alice'); expect(mockFindUnique).toHaveBeenCalledWith({ where: { id: 'u1' } }); });
import { withLatency } from 'slowdep'; import { PrismaClient } from '@prisma/client'; test('transfer rolls back if second operation fails', async () => { const prisma = new PrismaClient(); // Wrap the $transaction call itself — one latency hit for the whole transaction const slowTransaction = withLatency( prisma.$transaction.bind(prisma), { p50: 15, p99: 400 } // transactions have higher latency ); await expect( slowTransaction(async (tx) => { await tx.account.update({ where: { id: 'a1' }, data: { balance: { decrement: 100 } } }); throw new Error('simulated failure mid-transaction'); }) ).rejects.toThrow('simulated failure'); // Verify rollback: balance should be unchanged const account = await prisma.account.findUnique({ where: { id: 'a1' } }); expect(account.balance).toBe(500); // original value preserved await prisma.$disconnect(); });
Wrap $queryRaw for custom SQL
Prisma's $queryRaw and $executeRaw are plain async functions and can be wrapped directly.
import { PrismaClient, Prisma } from '@prisma/client'; import { withLatency } from 'slowdep'; const prisma = new PrismaClient(); // $queryRaw and $executeRaw are regular bound methods — wrap them directly const slowQueryRaw = withLatency(prisma.$queryRaw.bind(prisma), 'postgres'); const slowExecuteRaw = withLatency(prisma.$executeRaw.bind(prisma), 'postgres'); // Use the tagged template literal as normal const results = await slowQueryRaw<{ id: string; count: bigint }[]>` SELECT user_id AS id, COUNT(*) AS count FROM orders WHERE created_at > NOW() - INTERVAL '30 days' GROUP BY user_id ORDER BY count DESC LIMIT 10 `;
Environment-gated repository pattern
// src/db/userRepo.ts import { PrismaClient } from '@prisma/client'; import { withLatency } from 'slowdep'; const prisma = new PrismaClient(); const isDev = process.env.NODE_ENV !== 'production'; const wrap = <T extends any[], R>(fn: (...args: T) => Promise<R>) => isDev ? withLatency(fn, 'postgres') : fn; export const userRepo = { findById: wrap((id: string) => prisma.user.findUnique({ where: { id } })), findByEmail: wrap((email: string) => prisma.user.findUnique({ where: { email } })), findMany: wrap((take = 20, skip = 0) => prisma.user.findMany({ take, skip })), create: wrap((data: { email: string; name: string }) => prisma.user.create({ data })), updateById: wrap((id: string, data: { name?: string }) => prisma.user.update({ where: { id }, data })), };
Scenarios this integration helps you verify
- findUnique vs findFirst performance difference —
findUniqueuses a unique index; realistic latency helps you distinguish it fromfindFirston a non-unique field - Eager loading cost — Prisma's
includejoins can be expensive; use slowdep to feel the latency before optimizing withselect - Transaction timeout — nested transactions (
$transaction) with interactive or batch mode have a default timeout of 5s; verify your code handlesP2028correctly - Retry on connection error — Prisma throws
P1001on connection failure; verify your service retries with exponential backoff - Batch query efficiency —
prisma.$transaction([...])batches queries in one round-trip; compare the latency vs sequential awaits - N+1 detection via timing — Prisma DevTools warns about N+1, but realistic latency makes the problem visible in test durations
Fully typed repository
import { PrismaClient } from '@prisma/client'; import type { User, Post } from '@prisma/client'; import { withLatency } from 'slowdep'; const prisma = new PrismaClient(); // Return type is inferred: Promise<User | null> const findUser = withLatency( (id: string): Promise<User | null> => prisma.user.findUnique({ where: { id } }), 'postgres' ); // Return type: Promise<Post[]> const findPosts = withLatency( (authorId: string): Promise<Post[]> => prisma.post.findMany({ where: { authorId, published: true }, orderBy: { createdAt: 'desc' } }), 'postgres' ); // TypeScript enforces correct arguments and return types throughout const user: User | null = await findUser('clx1234'); const posts: Post[] = await findPosts('clx1234');