writing-skills
writing-skills
Use when creating new skills, editing existing skills, or verifying skills work before deployment
nodejs-prismaUse this skill when working with Prisma ORM — schema modeling, migrations, queries, relations, middleware, and deployment. This skill enforces: data model normalization, relation conventions, Prisma Client best practices, migration safety, and performance optimization. Requires @prisma/client. Do NOT use for: Mongoose, TypeORM, Drizzle ORM, or non-relationa…
Independent GitHub snapshot · Automated classification · Original documentation governs
Design database schemas, write performant queries, manage migrations, implement middleware, and optimize Prisma Client for production.
User request includes: prisma, prisma schema, prisma migrate, prisma client, prisma relation, prisma middleware, prisma query, prisma performance, prisma seed, prisma studio.
Schema definition, query examples, migration setup, middleware patterns, performance optimizations.
Produce artifact directly. No preamble, no postamble, no explanations.
4096 tokens
| Criterion | Prisma | Drizzle ORM | TypeORM | Knex |
|---|---|---|---|---|
| Type safety | Full (generated) | Full (inferred) | Partial | None |
| Migration system | Prisma Migrate | Drizzle Kit | TypeORM migrations | Knex migrations |
| Query builder | Declarative (Prisma Client) | SQL-like | Active Record / Data Mapper | SQL builder |
| Relation handling | Include / select | Joins explicit | relations / find | Manual JOINs |
| Middleware/hooks | Extensions (v5+) | Middleware | Subscribers | Raw Knex plugins |
| Performance | Moderate (mapped layer) | High (thin wrapper) | Moderate | High |
| Bundle size | Large (generated client) | Tiny (tree-shakeable) | Large | Moderate |
Decision: Full type safety + auto-complete → Prisma. Maximum performance + SQL control → Drizzle. Active Record familiarity → TypeORM.
| Aspect | Prisma Schema | Raw SQL |
|---|---|---|
| Source of truth | schema.prisma | migrations |
| Readability | Declarative, concise | Verbose |
| Index management | @@index decorators | CREATE INDEX |
| Enum support | Native (enum keyword) | CREATE TYPE |
| Composite keys | @@id([field1, field2]) | Composite PK |
Decision: Prisma-first project → use schema.prisma as source of truth. Existing DB → introspect with prisma db pull.
// prisma/schema.prisma
generator client {
provider = "prisma-client-js"
previewFeatures = ["extendedWhereUnique"]
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
enum UserRole {
ADMIN
USER
MODERATOR
}
model User {
id String @id @default(uuid())
email String @unique
name String
role UserRole @default(USER)
active Boolean @default(true)
profile Profile?
posts Post[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([email, active])
@@map("users")
}
model Profile {
id String @id @default(uuid())
bio String?
avatar String?
userId String @unique
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@map("profiles")
}
model Post {
id String @id @default(uuid())
title String
content String?
published Boolean @default(false)
authorId String
author User @relation(fields: [authorId], references: [id], onDelete: Cascade)
tags Tag[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([authorId, published])
@@map("posts")
}
model Tag {
id String @id @default(uuid())
name String @unique
posts Post[]
@@map("tags")
}
model PostTag {
postId String
tagId String
post Post @relation(fields: [postId], references: [id], onDelete: Cascade)
tag Tag @relation(fields: [tagId], references: [id], onDelete: Cascade)
@@id([postId, tagId])
@@map("post_tags")
}
// src/repositories/user.repository.ts
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
// SELECT with specific fields
export async function findUserById(id: string) {
return prisma.user.findUnique({
where: { id },
select: { id: true, name: true, email: true, role: true },
});
}
// Include relations (N+1 safe — Prisma batches)
export async function findUserWithProfile(id: string) {
return prisma.user.findUnique({
where: { id },
include: {
profile: true,
posts: {
where: { published: true },
select: { id: true, title: true, createdAt: true },
orderBy: { createdAt: 'desc' },
take: 10,
},
},
});
}
// Paginated list
export async function findUsers(page: number, limit: number) {
const [users, total] = await Promise.all([
prisma.user.findMany({
skip: (page - 1) * limit,
take: limit,
orderBy: { createdAt: 'desc' },
select: { id: true, name: true, email: true, role: true },
}),
prisma.user.count(),
]);
return { data: users, total, page, totalPages: Math.ceil(total / limit) };
}
// Create with nested write
export async function createUserWithProfile(data: CreateUserDto) {
return prisma.user.create({
data: {
email: data.email,
name: data.name,
profile: {
create: { bio: data.bio },
},
},
include: { profile: true },
});
}
// Batch update
export async function deactivateInactiveUsers(days: number) {
const cutoff = new Date(Date.now() - days * 86400000);
return prisma.user.updateMany({
where: { lastLoginAt: { lt: cutoff }, active: true },
data: { active: false },
});
}
// Delete cascade (defined in schema)
export async function deleteUser(id: string) {
return prisma.user.delete({ where: { id } });
}
// src/lib/prisma.ts
import { PrismaClient } from '@prisma/client';
const globalForPrisma = globalThis as unknown as { prisma: PrismaClient };
export const prisma = globalForPrisma.prisma ?? new PrismaClient({
log: process.env.NODE_ENV === 'development'
? ['query', 'info', 'warn', 'error']
: ['error'],
errorFormat: 'minimal',
});
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma;
# Create migration from schema changes
npx prisma migrate dev --name add_user_profile
# Apply to production
npx prisma migrate deploy
# Reset (dev only — drops data)
npx prisma migrate reset
# Generate client after schema change
npx prisma generate
# View migration status
npx prisma migrate status
// src/lib/prisma-extension.ts
import { PrismaClient } from '@prisma/client';
export const xprisma = new PrismaClient()
.$extends({
query: {
user: {
async create({ args, query }) {
// Auto-generate slug or hash password before create
return query(args);
},
async findUnique({ args, query }) {
// Soft-delete filter
args.where = { ...args.where, deletedAt: null };
return query(args);
},
},
},
result: {
user: {
fullName: {
needs: { firstName: true, lastName: true },
compute(user) {
return `${user.firstName} ${user.lastName}`;
},
},
},
},
model: {
user: {
async findByEmail(email: string) {
return prisma.user.findUnique({ where: { email } });
},
},
},
});
// Transfer funds with transaction
export async function transferFunds(fromId: string, toId: string, amount: number) {
return prisma.$transaction(async (tx) => {
const fromAccount = await tx.account.update({
where: { id: fromId },
data: { balance: { decrement: amount } },
});
if (fromAccount.balance < 0) {
throw new Error('Insufficient funds');
}
await tx.account.update({
where: { id: toId },
data: { balance: { increment: amount } },
});
await tx.transaction.create({
data: { fromId, toId, amount, type: 'TRANSFER' },
});
});
}
// prisma/seed.ts
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
async function main() {
const user = await prisma.user.create({
data: {
email: 'admin@test.com',
name: 'Admin',
role: 'ADMIN',
profile: { create: { bio: 'System admin' } },
posts: {
create: [
{ title: 'First Post', content: 'Hello world', published: true },
{ title: 'Draft', content: 'Not yet published' },
],
},
},
});
console.log('Seeded user:', user.id);
}
main()
.catch(e => { console.error(e); process.exit(1); })
.finally(() => prisma.$disconnect());
// Connection pool for serverless (Vercel, 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 });
select over include when only specific fields neededDATABASE_URL connection$queryRaw for complex aggregations@@index on frequently filtered/sorted columns@relation with onDelete: Cascade for referential integrity at DB levelinclude adds a JOINimport { PrismaClientKnownRequestError } from '@prisma/client/runtime/library';
try {
await prisma.user.create({ data: { email: 'dupe@test.com' } });
} catch (error) {
if (error instanceof PrismaClientKnownRequestError) {
if (error.code === 'P2002') { // Unique constraint violation
throw new ConflictError('Email already exists');
}
if (error.code === 'P2025') { // Record not found
throw new NotFoundError('User not found');
}
}
throw error;
}
| Anti-Pattern | Why | Fix |
|---|---|---|
| Full object in select | Overfetches data, slower queries | Select only needed fields |
Nested create without createMany | Multiple round trips | Use createMany for batch inserts |
Missing @updatedAt | No auto-update timestamp | Always add @updatedAt on mutable models |
| No connection pooling for serverless | Cold starts, connection exhaustion | Use Prisma Accelerate or pgBouncer |
| N+1 via loop queries | Sequential DB calls | Use include or batch with findMany |
| Schema drift (manual DB changes) | Out of sync with Prisma schema | Always use Prisma Migrate |
$queryRawUnsafe) risk SQL injection — use $queryRaw with parameterized templates.env — never committed to repo@map for column obfuscation not needed — use DB-level encryptionselect to avoid exposing sensitive fields (password hash, etc.)import { PrismaClient } from '@prisma/client';
import { vi, describe, it, expect, beforeAll, afterAll } from 'vitest';
const prisma = new PrismaClient();
describe('User Repository', () => {
beforeAll(async () => {
await prisma.$executeRawUnsafe('TRUNCATE TABLE users CASCADE');
});
afterAll(async () => {
await prisma.$disconnect();
});
it('should create and find user', async () => {
const user = await createUser({ email: 'test@test.com', name: 'Test' });
const found = await findUserById(user.id);
expect(found?.email).toBe('test@test.com');
});
});
Use separate test database with test user. Use prisma migrate deploy in CI. Use @prisma/nextjs-monorepo-workaround-plugin for monorepos.
prisma migrate dev after every schema change.prisma generate after every pull/sync — always regenerate client.select over include for production queries — minimize data transfer.deletedAt + middleware filter — never hard delete user data.$transaction for atomic multi-table operations.$extends for cross-cutting concerns (soft delete, audit, computed fields).prisma.$disconnect() in serverless handlers — let adapter handle pooling.Hand off to backend/nodejs/drizzle/SKILL.md for Drizzle ORM patterns or backend/nodejs/patterns/SKILL.md for advanced Node patterns.
function createModule<T>(config: ModuleConfig): T { const dependencies = initializeDependencies(config); const module = new Module(dependencies); module.hooks.onInit(); return module as T; }
class ConfigBuilder { private config: AppConfig = new AppConfig(); withDatabase(url: string): ConfigBuilder { ... } withCache(ttl: number): ConfigBuilder { ... } withLogging(level: string): ConfigBuilder { ... } build(): AppConfig { return this.config; } }
| Metric | Threshold | Severity | Action |
|---|---|---|---|
| Error rate | > 1% | Critical | Rollback or fix |
| p95 latency | > 500ms | Warning | Profile and optimize |
| Uptime | < 99.9% | Critical | Investigate infrastructure |
| Memory usage | > 80% | Warning | Check for leaks |
| CPU usage | > 80% | Warning | Scale up or optimize |