← Back to list

database-patterns
by Wania-Kazmi
Autonomous project generator for Claude Code. Write requirements, run one command, get a complete project with custom skills, agents, hooks, TDD, 80%+ coverage, and security-reviewed code.
⭐ 2🍴 0📅 Jan 24, 2026
SKILL.md
name: database-patterns description: | Database design patterns, SQL best practices, ORM usage (Prisma/Drizzle), and migration strategies. Use when designing schemas, writing queries, or optimizing database operations. allowed-tools: Read, Write, Edit, Bash, Grep, Glob
Database Patterns & Best Practices
Schema Design Principles
1. Normalization (3NF minimum)
- No repeating groups
- No partial dependencies
- No transitive dependencies
2. Use UUIDs vs Auto-Increment
-- GOOD: UUID for distributed systems
id UUID PRIMARY KEY DEFAULT gen_random_uuid()
-- OK: Auto-increment for simple cases
id SERIAL PRIMARY KEY
3. Timestamps on Every Table
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(100) NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
Prisma ORM Patterns
Schema Definition
// prisma/schema.prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model User {
id String @id @default(uuid())
email String @unique
name String
role Role @default(USER)
posts Post[]
profile Profile?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([email])
@@map("users")
}
model Post {
id String @id @default(uuid())
title String
content String?
published Boolean @default(false)
author User @relation(fields: [authorId], references: [id])
authorId String
tags Tag[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([authorId])
@@index([published])
@@map("posts")
}
enum Role {
USER
ADMIN
}
CRUD Operations
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
// Create
async function createUser(data: { name: string; email: string }) {
return prisma.user.create({
data,
select: {
id: true,
name: true,
email: true,
createdAt: true
}
})
}
// Read with relations
async function getUserWithPosts(id: string) {
return prisma.user.findUnique({
where: { id },
include: {
posts: {
where: { published: true },
orderBy: { createdAt: 'desc' },
take: 10
},
profile: true
}
})
}
// Update
async function updateUser(id: string, data: Partial<User>) {
return prisma.user.update({
where: { id },
data
})
}
// Delete (soft delete pattern)
async function deleteUser(id: string) {
return prisma.user.update({
where: { id },
data: { deletedAt: new Date() }
})
}
Transactions
// Interactive transaction
async function transferFunds(fromId: string, toId: string, amount: number) {
return prisma.$transaction(async (tx) => {
const from = await tx.account.update({
where: { id: fromId },
data: { balance: { decrement: amount } }
})
if (from.balance < 0) {
throw new Error('Insufficient funds')
}
await tx.account.update({
where: { id: toId },
data: { balance: { increment: amount } }
})
return tx.transaction.create({
data: { fromId, toId, amount }
})
})
}
// Sequential transaction
async function createUserWithProfile(data: CreateUserInput) {
return prisma.$transaction([
prisma.user.create({ data: data.user }),
prisma.profile.create({ data: data.profile })
])
}
Pagination
// Cursor-based (recommended for large datasets)
async function getUsers(cursor?: string, limit = 20) {
const users = await prisma.user.findMany({
take: limit + 1,
cursor: cursor ? { id: cursor } : undefined,
orderBy: { createdAt: 'desc' },
skip: cursor ? 1 : 0 // Skip the cursor itself
})
const hasMore = users.length > limit
const data = hasMore ? users.slice(0, -1) : users
return {
data,
nextCursor: hasMore ? data[data.length - 1].id : null
}
}
// Offset-based
async function getUsersWithOffset(page = 1, limit = 20) {
const [users, total] = await Promise.all([
prisma.user.findMany({
skip: (page - 1) * limit,
take: limit
}),
prisma.user.count()
])
return {
data: users,
meta: { total, page, limit, totalPages: Math.ceil(total / limit) }
}
}
Query Optimization
Avoid N+1 Queries
// BAD: N+1 problem
const users = await prisma.user.findMany()
for (const user of users) {
const posts = await prisma.post.findMany({
where: { authorId: user.id }
})
}
// GOOD: Include in single query
const users = await prisma.user.findMany({
include: { posts: true }
})
// GOOD: Select only needed fields
const users = await prisma.user.findMany({
select: {
id: true,
name: true,
posts: {
select: {
id: true,
title: true
}
}
}
})
Use Indexes Properly
model Post {
id String @id
authorId String
status Status
createdAt DateTime
// Compound index for common query patterns
@@index([authorId, status])
@@index([status, createdAt])
}
-- For frequently queried columns
CREATE INDEX idx_posts_author_status ON posts(author_id, status);
-- For text search
CREATE INDEX idx_posts_title_gin ON posts USING gin(to_tsvector('english', title));
-- For JSON columns
CREATE INDEX idx_metadata_gin ON posts USING gin(metadata);
Batch Operations
// Batch create
await prisma.user.createMany({
data: users,
skipDuplicates: true
})
// Batch update with raw SQL (when needed)
await prisma.$executeRaw`
UPDATE posts
SET status = 'archived'
WHERE created_at < NOW() - INTERVAL '1 year'
`
// Batch delete
await prisma.user.deleteMany({
where: {
deletedAt: { not: null },
deletedAt: { lt: thirtyDaysAgo }
}
})
Migration Patterns
Prisma Migrations
# Create migration
npx prisma migrate dev --name add_user_role
# Apply in production
npx prisma migrate deploy
# Reset database (dev only)
npx prisma migrate reset
Safe Schema Changes
// Step 1: Add nullable column
model User {
newField String? // nullable first
}
// Step 2: Backfill data
await prisma.$executeRaw`
UPDATE users SET new_field = 'default' WHERE new_field IS NULL
`
// Step 3: Make non-nullable
model User {
newField String @default("default")
}
Rollback Strategy
-- Always create down migrations
-- up.sql
ALTER TABLE users ADD COLUMN phone VARCHAR(20);
-- down.sql
ALTER TABLE users DROP COLUMN phone;
Connection Management
Connection Pooling
// Singleton pattern for Prisma
const globalForPrisma = globalThis as unknown as {
prisma: PrismaClient | undefined
}
export const prisma = globalForPrisma.prisma ?? new PrismaClient({
log: ['error', 'warn'],
datasources: {
db: {
url: process.env.DATABASE_URL
}
}
})
if (process.env.NODE_ENV !== 'production') {
globalForPrisma.prisma = prisma
}
Serverless Configuration
// For serverless (Vercel, AWS Lambda)
import { PrismaClient } from '@prisma/client'
import { Pool } from '@neondatabase/serverless'
import { PrismaNeon } from '@prisma/adapter-neon'
const pool = new Pool({ connectionString: process.env.DATABASE_URL })
const adapter = new PrismaNeon(pool)
const prisma = new PrismaClient({ adapter })
Soft Delete Pattern
model User {
id String @id @default(uuid())
email String @unique
deletedAt DateTime?
@@index([deletedAt])
}
// Middleware for automatic filtering
prisma.$use(async (params, next) => {
if (params.model === 'User') {
if (params.action === 'findMany' || params.action === 'findFirst') {
params.args.where = {
...params.args.where,
deletedAt: null
}
}
}
return next(params)
})
// Soft delete
async function softDelete(id: string) {
return prisma.user.update({
where: { id },
data: { deletedAt: new Date() }
})
}
// Hard delete (permanent)
async function hardDelete(id: string) {
return prisma.user.delete({ where: { id } })
}
Audit Trail Pattern
model AuditLog {
id String @id @default(uuid())
entityType String
entityId String
action String // CREATE, UPDATE, DELETE
changes Json?
userId String?
createdAt DateTime @default(now())
@@index([entityType, entityId])
@@index([userId])
@@index([createdAt])
}
// Middleware for automatic audit logging
prisma.$use(async (params, next) => {
const result = await next(params)
if (['create', 'update', 'delete'].includes(params.action)) {
await prisma.auditLog.create({
data: {
entityType: params.model!,
entityId: result.id,
action: params.action.toUpperCase(),
changes: params.args.data,
userId: getCurrentUserId()
}
})
}
return result
})
Multi-Tenant Pattern
model Organization {
id String @id @default(uuid())
name String
users User[]
posts Post[]
}
model User {
id String @id @default(uuid())
organization Organization @relation(fields: [organizationId], references: [id])
organizationId String
@@index([organizationId])
}
// Row-level security with Prisma extension
const prismaWithTenant = prisma.$extends({
query: {
$allModels: {
async $allOperations({ args, query, model }) {
const tenantId = getTenantId()
if (tenantId && hasTenantField(model)) {
args.where = { ...args.where, organizationId: tenantId }
}
return query(args)
}
}
}
})
Raw SQL When Needed
// Complex aggregations
const stats = await prisma.$queryRaw<Stats[]>`
SELECT
DATE_TRUNC('day', created_at) as date,
COUNT(*) as count,
SUM(amount) as total
FROM transactions
WHERE created_at >= ${startDate}
GROUP BY DATE_TRUNC('day', created_at)
ORDER BY date DESC
`
// Full-text search
const results = await prisma.$queryRaw<Post[]>`
SELECT * FROM posts
WHERE to_tsvector('english', title || ' ' || content)
@@ plainto_tsquery('english', ${searchTerm})
ORDER BY ts_rank(
to_tsvector('english', title || ' ' || content),
plainto_tsquery('english', ${searchTerm})
) DESC
LIMIT ${limit}
`
Checklist
- UUIDs for distributed systems
- Timestamps on all tables
- Proper indexes for query patterns
- N+1 queries avoided (use include/join)
- Transactions for multi-step operations
- Soft delete where appropriate
- Connection pooling configured
- Migrations tested (up and down)
- Audit logging for sensitive data
- Query performance monitored
Score
Total Score
65/100
Based on repository quality metrics
✓SKILL.md
SKILL.mdファイルが含まれている
+20
○LICENSE
ライセンスが設定されている
0/10
✓説明文
100文字以上の説明がある
+10
○人気
GitHub Stars 100以上
0/15
✓最近の活動
1ヶ月以内に更新
+10
○フォーク
10回以上フォークされている
0/5
✓Issue管理
オープンIssueが50未満
+5
✓言語
プログラミング言語が設定されている
+5
✓タグ
1つ以上のタグが設定されている
+5
Reviews
💬
Reviews coming soon
