prisma-patterns

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Prisma Patterns

Prisma 模式

Production patterns and non-obvious traps for Prisma ORM in TypeScript backends. Tested against Prisma 5.x and 6.x. Some behaviors differ from Prisma 4.
Check the Prisma version before applying version-specific patterns:
bash
npx prisma --version
Prisma 5 introduced
relationJoins
, which can load relations via JOIN rather than separate queries depending on query strategy and configuration. The
omit
field modifier and
prisma.$extends
Client Extensions API were also added. Note:
relationJoins
can cause row explosion on large 1:N relations or deep nested
include
— benchmark both approaches when relations may return many rows per parent.
TypeScript后端中Prisma ORM的生产级模式与易踩陷阱。 已针对 Prisma 5.x 和 6.x 测试。部分行为与 Prisma 4 不同。
在应用特定版本的模式前,请检查 Prisma 版本:
bash
npx prisma --version
Prisma 5 引入了
relationJoins
,它可以根据查询策略和配置,通过JOIN而非单独查询来加载关联数据。同时还新增了
omit
字段修饰符和
prisma.$extends
客户端扩展API。注意:在大型1:N关联或深度嵌套
include
场景下,
relationJoins
可能导致数据行数暴增——当关联可能为每个父级返回大量数据时,请对两种方式进行基准测试。

When to Activate

适用场景

  • Designing or modifying Prisma schema models and relations
  • Writing queries, transactions, or pagination logic
  • Using
    updateMany
    ,
    deleteMany
    , or any bulk operation
  • Running or planning database migrations
  • Deploying to serverless environments (Vercel, Lambda, Cloudflare Workers)
  • Implementing soft delete or multi-tenant row filtering
  • 设计或修改Prisma schema模型与关联关系
  • 编写查询、事务或分页逻辑
  • 使用
    updateMany
    deleteMany
    或任何批量操作
  • 执行或规划数据库迁移
  • 部署到无服务器环境(Vercel、Lambda、Cloudflare Workers)
  • 实现软删除或多租户数据行过滤

Core Concepts

核心概念

ID Strategy

ID 策略

StrategyUse WhenAvoid When
@default(cuid())
Default choice — URL-safe, sortable, no collisionsSequential IDs needed for external systems
@default(uuid())
Interoperability with non-Prisma systems requiredHigh-write tables (random UUIDs fragment B-tree indexes)
@default(autoincrement())
Internal join tables, audit logsPublic-facing IDs (exposes record count)
策略适用场景避免场景
@default(cuid())
默认选择——URL安全、可排序、无冲突需要为外部系统使用连续ID时
@default(uuid())
需要与非Prisma系统互操作时高写入量表格(随机UUID会导致B树索引碎片化)
@default(autoincrement())
内部关联表、审计日志面向公众的ID(会暴露记录数量)

Schema Defaults

Schema 默认配置

prisma
model User {
  id        String    @id @default(cuid())
  email     String    @unique  // @unique already creates an index — no @@index needed
  name      String
  role      Role      @default(USER)
  posts     Post[]
  createdAt DateTime  @default(now())
  updatedAt DateTime  @updatedAt
  deletedAt DateTime?

  @@index([createdAt])
  @@index([deletedAt, createdAt]) // composite for soft-delete + sort queries
}
  • Add
    @@index
    on every foreign key and column used in
    WHERE
    or
    ORDER BY
    .
  • Declare
    deletedAt DateTime?
    upfront when soft delete is a foreseeable requirement — adding it later requires a migration on a live table.
  • updatedAt @updatedAt
    is set automatically by Prisma on
    update
    and
    upsert
    only (see Anti-Patterns for bulk update trap).
prisma
model User {
  id        String    @id @default(cuid())
  email     String    @unique  // @unique已自动创建索引——无需额外添加@@index
  name      String
  role      Role      @default(USER)
  posts     Post[]
  createdAt DateTime  @default(now())
  updatedAt DateTime  @updatedAt
  deletedAt DateTime?

  @@index([createdAt])
  @@index([deletedAt, createdAt]) // 用于软删除+排序查询的复合索引
}
  • 为每个外键和用于
    WHERE
    ORDER BY
    的字段添加
    @@index
  • 当可预见需要软删除时,提前声明
    deletedAt DateTime?
    ——后续添加需要对在线表格执行迁移。
  • updatedAt @updatedAt
    仅在
    update
    upsert
    操作时由Prisma自动设置(反模式部分会介绍批量更新陷阱)。

include
vs
select

include
vs
select

include
select
ReturnsAll scalar fields + specified relationsOnly specified fields
Use whenYou need most fields plus a relationHot paths, large tables, avoiding over-fetch
PerformanceMay over-fetch on wide tablesMinimal payload, faster on large datasets
Prisma 5 noteUses JOIN by default (
relationJoins
)
Same
ts
// include — all columns + relation
const user = await prisma.user.findUnique({
  where: { id },
  include: { posts: { select: { id: true, title: true } } },
});

// select — explicit allowlist
const user = await prisma.user.findUnique({
  where: { id },
  select: { id: true, email: true, name: true },
});
Never return raw Prisma entities from API responses — map to response DTOs to control exposed fields:
ts
// BAD: leaks passwordHash, deletedAt, internal fields
return await prisma.user.findUniqueOrThrow({ where: { id } });

// GOOD: explicit DTO mapping
const user = await prisma.user.findUniqueOrThrow({ where: { id } });
return { id: user.id, name: user.name, email: user.email };
include
select
返回内容所有标量字段 + 指定关联仅指定字段
适用场景需要大部分字段加关联数据时热点路径、大型表格、避免过度获取数据时
性能在宽表中可能过度获取数据负载最小化,在大型数据集上速度更快
Prisma 5 注意事项默认使用JOIN(
relationJoins
无变化
ts
// include — 所有列 + 关联数据
const user = await prisma.user.findUnique({
  where: { id },
  include: { posts: { select: { id: true, title: true } } },
});

// select — 显式白名单
const user = await prisma.user.findUnique({
  where: { id },
  select: { id: true, email: true, name: true },
});
永远不要从API响应中返回原始Prisma实体——映射为响应DTO以控制暴露的字段:
ts
// 错误:会泄露passwordHash、deletedAt等内部字段
return await prisma.user.findUniqueOrThrow({ where: { id } });

// 正确:显式DTO映射
const user = await prisma.user.findUniqueOrThrow({ where: { id } });
return { id: user.id, name: user.name, email: user.email };

Transaction Form Selection

事务形式选择

SituationUse
Independent operations, no inter-dependencyArray form
Later step depends on earlier resultInteractive form
External calls (email, HTTP) involvedOutside transaction entirely
ts
// Array form — batched in one round trip
const [user, post] = await prisma.$transaction([
  prisma.user.update({ where: { id }, data: { name } }),
  prisma.post.create({ data: { title, authorId: id } }),
]);

// Interactive form — use tx client only, never the outer prisma client
const post = await prisma.$transaction(async (tx) => {
  const user = await tx.user.findUniqueOrThrow({ where: { id } });
  if (user.role !== 'ADMIN') throw new Error('Forbidden');
  return tx.post.create({ data: { title, authorId: user.id } });
});
场景使用方式
独立操作、无相互依赖数组形式
后续步骤依赖前期结果交互式形式
涉及外部调用(邮件、HTTP)完全在事务外执行
ts
// 数组形式 — 批量单次往返
const [user, post] = await prisma.$transaction([
  prisma.user.update({ where: { id }, data: { name } }),
  prisma.post.create({ data: { title, authorId: id } }),
]);

// 交互式形式 — 仅使用tx客户端,绝不要使用外部prisma客户端
const post = await prisma.$transaction(async (tx) => {
  const user = await tx.user.findUniqueOrThrow({ where: { id } });
  if (user.role !== 'ADMIN') throw new Error('Forbidden');
  return tx.post.create({ data: { title, authorId: user.id } });
});

PrismaClient Singleton

PrismaClient 单例

Each
PrismaClient
instance opens its own connection pool. Instantiate once.
ts
// 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', 'error'] : ['error'],
  });

if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma;
The
globalThis
pattern prevents duplicate instances during hot reload (Next.js, nodemon, ts-node-dev).
每个
PrismaClient
实例都会打开自己的连接池。请只实例化一次。
ts
// 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', 'error'] : ['error'],
  });

if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma;
globalThis
模式可防止热重载期间(Next.js、nodemon、ts-node-dev)创建重复实例。

N+1 Problem

N+1 问题

Loading relations inside a loop issues one query per row.
ts
// BAD: N+1 — one extra query per user
const users = await prisma.user.findMany();
for (const user of users) {
  const posts = await prisma.post.findMany({ where: { authorId: user.id } });
}

// GOOD: single query
const users = await prisma.user.findMany({ include: { posts: true } });
With Prisma 5+
relationJoins
, the
include
form uses a single JOIN. On large 1:N sets this may increase result set size — benchmark both approaches if the relation can return many rows per parent.
在循环内加载关联数据会为每一行发起一次查询。
ts
// 错误:N+1 — 每个用户额外发起一次查询
const users = await prisma.user.findMany();
for (const user of users) {
  const posts = await prisma.post.findMany({ where: { authorId: user.id } });
}

// 正确:单次查询
const users = await prisma.user.findMany({ include: { posts: true } });
在Prisma 5+的
relationJoins
下,
include
形式会使用单次JOIN。在大型1:N集合场景下,这可能会增加结果集大小——当关联可能为每个父级返回大量数据时,请对两种方式进行基准测试。

Code Examples

代码示例

Cursor Pagination (preferred for feeds and large datasets)

游标分页(适用于信息流和大型数据集)

ts
async function getPosts(cursor?: string, limit = 20) {
  const items = await prisma.post.findMany({
    where: { published: true },
    orderBy: [
      { createdAt: 'desc' },
      { id: 'desc' }, // secondary sort prevents unstable pagination on duplicate timestamps
    ],
    take: limit + 1,
    ...(cursor && { cursor: { id: cursor }, skip: 1 }),
  });

  const hasNextPage = items.length > limit;
  if (hasNextPage) items.pop();

  return { items, nextCursor: hasNextPage ? items[items.length - 1].id : null };
}
Fetch
limit + 1
and pop — canonical way to detect
hasNextPage
without an extra count query. Always include a unique field (e.g.
id
) as a secondary
orderBy
to prevent unstable pagination when multiple rows share the same timestamp. Use offset pagination only when users need to jump to arbitrary pages (admin tables).
ts
async function getPosts(cursor?: string, limit = 20) {
  const items = await prisma.post.findMany({
    where: { published: true },
    orderBy: [
      { createdAt: 'desc' },
      { id: 'desc' }, // 二级排序可避免重复时间戳导致的分页不稳定
    ],
    take: limit + 1,
    ...(cursor && { cursor: { id: cursor }, skip: 1 }),
  });

  const hasNextPage = items.length > limit;
  if (hasNextPage) items.pop();

  return { items, nextCursor: hasNextPage ? items[items.length - 1].id : null };
}
获取
limit + 1
条数据后弹出最后一条——这是无需额外计数查询即可检测
hasNextPage
的标准方式。始终将唯一字段(如
id
)作为二级
orderBy
,以防止多行共享同一时间戳时出现分页不稳定。仅当用户需要跳转到任意页面(如后台表格)时才使用偏移分页。

Soft Delete

软删除

ts
// Always filter explicitly — do not rely on middleware (hides behavior, hard to debug)
const activeUsers = await prisma.user.findMany({ where: { deletedAt: null } });

await prisma.user.update({ where: { id }, data: { deletedAt: new Date() } });
await prisma.user.update({ where: { id }, data: { deletedAt: null } }); // restore
ts
// 始终显式过滤——不要依赖中间件(会隐藏行为,难以调试)
const activeUsers = await prisma.user.findMany({ where: { deletedAt: null } });

await prisma.user.update({ where: { id }, data: { deletedAt: new Date() } });
await prisma.user.update({ where: { id }, data: { deletedAt: null } }); // 恢复

Error Handling

错误处理

ts
import { Prisma } from '@prisma/client';

try {
  await prisma.user.create({ data: { email } });
} catch (e) {
  if (e instanceof Prisma.PrismaClientKnownRequestError) {
    if (e.code === 'P2002') throw new ConflictError('Email already exists');
    if (e.code === 'P2025') throw new NotFoundError('Record not found');
    if (e.code === 'P2003') throw new BadRequestError('Referenced record does not exist');
  }
  throw e;
}
Common codes:
P2002
unique violation ·
P2025
not found ·
P2003
foreign key violation.
Catch at the service boundary and translate to domain errors. Never expose raw Prisma messages to API consumers.
ts
import { Prisma } from '@prisma/client';

try {
  await prisma.user.create({ data: { email } });
} catch (e) {
  if (e instanceof Prisma.PrismaClientKnownRequestError) {
    if (e.code === 'P2002') throw new ConflictError('邮箱已存在');
    if (e.code === 'P2025') throw new NotFoundError('记录不存在');
    if (e.code === 'P2003') throw new BadRequestError('引用的记录不存在');
  }
  throw e;
}
常见错误码:
P2002
唯一约束冲突 ·
P2025
未找到 ·
P2003
外键约束冲突。
在服务层捕获错误并转换为领域错误。永远不要向API消费者暴露原始Prisma错误信息。

Connection Pool — Serverless

连接池——无服务器环境

Embed connection params directly in
DATABASE_URL
— string concatenation breaks if the URL already has query parameters (e.g.
?schema=public
):
bash
undefined
直接在
DATABASE_URL
中嵌入连接参数——如果URL已包含查询参数(如
?schema=public
),字符串拼接会失效:
bash
undefined

.env — preferred: embed params in the URL

.env — 推荐:在URL中嵌入参数

DATABASE_URL="postgresql://user:pass@host/db?connection_limit=1&pool_timeout=20"
DATABASE_URL="postgresql://user:pass@host/db?connection_limit=1&pool_timeout=20"

With an external pooler (PgBouncer, Supabase pooler)

使用外部池化工具(PgBouncer、Supabase pooler)

DATABASE_URL="postgresql://user:pass@host/db?pgbouncer=true&connection_limit=1"

```ts
// Vercel, AWS Lambda, and similar serverless runtimes: cap pool to 1 per instance
// connection_limit and pool_timeout are controlled via DATABASE_URL
const prisma = new PrismaClient();
DATABASE_URL="postgresql://user:pass@host/db?pgbouncer=true&connection_limit=1"

```ts
// Vercel、AWS Lambda及类似无服务器运行时:将连接池限制为每个实例1个
// connection_limit和pool_timeout通过DATABASE_URL控制
const prisma = new PrismaClient();

Anti-Patterns

反模式

updateMany
returns a count, not records

updateMany
返回计数而非记录

ts
// BAD: result is { count: 2 } — users[0] is undefined
const users = await prisma.user.updateMany({ where: { role: 'GUEST' }, data: { role: 'USER' } });

// GOOD: capture IDs first, then update, then fetch only the affected rows
const targets = await prisma.user.findMany({
  where: { role: 'GUEST' },
  select: { id: true },
});
const ids = targets.map((u) => u.id);
await prisma.user.updateMany({ where: { id: { in: ids } }, data: { role: 'USER' } });
const updated = await prisma.user.findMany({ where: { id: { in: ids } } });
Same applies to
deleteMany
— returns
{ count: n }
, never the deleted rows.
ts
// 错误:结果为{ count: 2 } — users[0]未定义
const users = await prisma.user.updateMany({ where: { role: 'GUEST' }, data: { role: 'USER' } });

// 正确:先捕获ID,再更新,最后仅获取受影响的行
const targets = await prisma.user.findMany({
  where: { role: 'GUEST' },
  select: { id: true },
});
const ids = targets.map((u) => u.id);
await prisma.user.updateMany({ where: { id: { in: ids } }, data: { role: 'USER' } });
const updated = await prisma.user.findMany({ where: { id: { in: ids } } });
deleteMany
同理——返回
{ count: n }
,绝不会返回删除的行。

$transaction
interactive form times out after 5 seconds

$transaction
交互式形式5秒后超时

ts
// BAD: external call inside transaction exceeds 5s default → "Transaction already closed"
await prisma.$transaction(async (tx) => {
  const user = await tx.user.findUniqueOrThrow({ where: { id } });
  await sendWelcomeEmail(user.email); // external call
  await tx.user.update({ where: { id }, data: { emailSent: true } });
});

// GOOD: external calls outside the transaction
const user = await prisma.user.findUniqueOrThrow({ where: { id } });
await sendWelcomeEmail(user.email);
await prisma.user.update({ where: { id }, data: { emailSent: true } });

// Only raise timeout when bulk processing genuinely needs it
await prisma.$transaction(async (tx) => { ... }, { timeout: 30_000 });
ts
// 错误:事务内的外部调用超过5秒默认超时 → "Transaction already closed"
await prisma.$transaction(async (tx) => {
  const user = await tx.user.findUniqueOrThrow({ where: { id } });
  await sendWelcomeEmail(user.email); // 外部调用
  await tx.user.update({ where: { id }, data: { emailSent: true } });
});

// 正确:外部调用放在事务外
const user = await prisma.user.findUniqueOrThrow({ where: { id } });
await sendWelcomeEmail(user.email);
await prisma.user.update({ where: { id }, data: { emailSent: true } });

// 仅在批量处理确实需要时才延长超时
await prisma.$transaction(async (tx) => { ... }, { timeout: 30_000 });

migrate dev
can reset the database

migrate dev
可能重置数据库

migrate dev
detects schema drift and may prompt to reset the DB, dropping all data.
bash
undefined
migrate dev
检测到schema漂移时,可能会提示重置数据库,删除所有数据。
bash
undefined

NEVER on shared dev, staging, or production

绝不要在共享开发、预发布或生产环境使用

npx prisma migrate dev --name add_column
npx prisma migrate dev --name add_column

Safe everywhere except local solo dev

除本地单人开发外,所有环境都安全

npx prisma migrate deploy
npx prisma migrate deploy

Check drift without applying

检查漂移而不应用

npx prisma migrate diff
--from-migrations ./prisma/migrations
--to-schema-datamodel ./prisma/schema.prisma
--shadow-database-url "$SHADOW_DATABASE_URL"
undefined
npx prisma migrate diff
--from-migrations ./prisma/migrations
--to-schema-datamodel ./prisma/schema.prisma
--shadow-database-url "$SHADOW_DATABASE_URL"
undefined

Manually editing a migration file breaks future deploys

手动编辑迁移文件会破坏未来部署

Prisma checksums every migration file. Editing after apply causes
P3006 checksum mismatch
on every environment where the original already ran. Create a new migration instead.
Prisma会为每个迁移文件生成校验和。应用后编辑会导致已运行原始迁移的所有环境出现
P3006 checksum mismatch
错误。请创建新的迁移文件替代。

Breaking schema changes require multi-step migration

破坏性schema变更需要多步迁移

Adding
NOT NULL
to an existing column or renaming a column in one migration will lock the table or drop data. Use expand-and-contract:
bash
undefined
在现有字段添加
NOT NULL
约束或重命名字段的单次迁移会锁定表格或丢失数据。请使用扩展-收缩法:
bash
undefined

Step 1: create migration locally, then deploy

步骤1:本地创建迁移,然后部署

npx prisma migrate dev --name add_new_column # local only npx prisma migrate deploy # staging / production

```ts
// Step 2: backfill data (run in a script or migration job, not in the shell)
await prisma.user.updateMany({ data: { newColumn: derivedValue } });
bash
undefined
npx prisma migrate dev --name add_new_column # 仅本地 npx prisma migrate deploy # 预发布/生产

```ts
// 步骤2:回填数据(在脚本或迁移任务中运行,不要在shell中)
await prisma.user.updateMany({ data: { newColumn: derivedValue } });
bash
undefined

Step 3: create the NOT NULL constraint migration locally, then deploy

步骤3:本地创建添加NOT NULL约束的迁移,然后部署

npx prisma migrate dev --name make_new_column_required # local only npx prisma migrate deploy # staging / production
undefined
npx prisma migrate dev --name make_new_column_required # 仅本地 npx prisma migrate deploy # 预发布/生产
undefined

@updatedAt
does not fire on
updateMany

@updatedAt
updateMany
中不会触发

@updatedAt
is set automatically only on
update
and
upsert
. Bulk writes leave it stale.
ts
// BAD: updatedAt stays at its old value
await prisma.post.updateMany({ where: { authorId }, data: { published: true } });

// GOOD
await prisma.post.updateMany({
  where: { authorId },
  data: { published: true, updatedAt: new Date() },
});
@updatedAt
仅在
update
upsert
操作时自动设置。批量写入会使其保持旧值。
ts
// 错误:updatedAt保持旧值
await prisma.post.updateMany({ where: { authorId }, data: { published: true } });

// 正确
await prisma.post.updateMany({
  where: { authorId },
  data: { published: true, updatedAt: new Date() },
});

Soft delete +
findUniqueOrThrow
leaks deleted records

软删除 +
findUniqueOrThrow
会泄露已删除记录

findUniqueOrThrow
throws
P2025
only when the row does not exist in the DB. Soft-deleted rows still exist and are returned without error.
findUniqueOrThrow
requires a unique constraint field in
where
— adding
deletedAt: null
alongside
id
breaks the type because
{ id, deletedAt }
is not a compound unique constraint. Use
findFirstOrThrow
instead.
ts
// BAD: returns soft-deleted user
const user = await prisma.user.findUniqueOrThrow({ where: { id } });

// BAD: Prisma type error — { id, deletedAt } is not a unique constraint
const user = await prisma.user.findUniqueOrThrow({ where: { id, deletedAt: null } });

// GOOD: findFirstOrThrow supports arbitrary where conditions
const user = await prisma.user.findFirstOrThrow({ where: { id, deletedAt: null } });
findUniqueOrThrow
仅在数据库中不存在该行时抛出
P2025
。软删除的行仍然存在,会无错误返回。
findUniqueOrThrow
要求
where
中包含唯一约束字段——将
deletedAt: null
id
一起添加会触发类型错误,因为
{ id, deletedAt }
不是复合唯一约束。请改用
findFirstOrThrow
ts
// 错误:返回软删除的用户
const user = await prisma.user.findUniqueOrThrow({ where: { id } });

// 错误:Prisma类型错误——{ id, deletedAt }不是唯一约束
const user = await prisma.user.findUniqueOrThrow({ where: { id, deletedAt: null } });

// 正确:findFirstOrThrow支持任意where条件
const user = await prisma.user.findFirstOrThrow({ where: { id, deletedAt: null } });

deleteMany
without
where
deletes every row

where
deleteMany
会删除所有行

ts
// BAD: silently wipes the table
await prisma.post.deleteMany();

// GOOD
await prisma.post.deleteMany({ where: { authorId: userId } });
ts
// 错误:静默清空表格
await prisma.post.deleteMany();

// 正确
await prisma.post.deleteMany({ where: { authorId: userId } });

Best Practices

最佳实践

RuleReason
migrate deploy
in CI/CD,
migrate dev
only locally
migrate dev
can reset the DB on drift
Map entities to response DTOsPrevents leaking internal fields
Catch
PrismaClientKnownRequestError
at service boundary
Translate to domain errors
Prefer
*OrThrow
methods over manual null checks
Throws P2025 automatically; use
findFirstOrThrow
when filtering non-unique fields
connection_limit=1
+ external pooler in serverless
Prevents connection exhaustion
Always provide
where
on
deleteMany
Prevents accidental table wipe
Set
updatedAt: new Date()
manually in
updateMany
@updatedAt
skips bulk writes
规则原因
在CI/CD中使用
migrate deploy
,仅在本地使用
migrate dev
migrate dev
在检测到漂移时可能重置数据库
将实体映射为响应DTO防止泄露内部字段
在服务层捕获
PrismaClientKnownRequestError
转换为领域错误
优先使用
*OrThrow
方法而非手动空检查
自动抛出P2025;过滤非唯一字段时使用
findFirstOrThrow
无服务器环境中使用
connection_limit=1
+ 外部池化工具
防止连接耗尽
deleteMany
时始终提供
where
条件
防止意外清空表格
updateMany
中手动设置
updatedAt: new Date()
@updatedAt
会跳过批量写入

Related Skills

相关技能

  • nestjs-patterns
    — NestJS service layer that integrates Prisma
  • postgres-patterns
    — PostgreSQL-level indexing and connection tuning
  • database-migrations
    — multi-step migration planning for production
  • backend-patterns
    — general API and service layer design
  • nestjs-patterns
    — 集成Prisma的NestJS服务层
  • postgres-patterns
    — PostgreSQL层面的索引与连接调优
  • database-migrations
    — 生产环境的多步迁移规划
  • backend-patterns
    — 通用API与服务层设计