Skip to content
我们的赞助商
博客

用 Elysia 加速你的下一个 Prisma 服务器

放置在中心的三角棱镜
Prisma 是一个著名的 TypeScript ORM,以其出色的开发者体验而闻名。

它提供了类型安全的直观 API,让我们可以使用流畅、自然的语法与数据库交互。

编写数据库查询就像用 TypeScript 编写数据形状一样简单,利用自动补全功能,然后 Prisma 会处理其余部分,在后台生成高效的 SQL 查询并管理数据库连接。

Prisma 的一个突出特性是它与流行数据库的无缝集成,例如:

  • PostgreSQL
  • MySQL
  • SQLite
  • SQL Server
  • MongoDB
  • CockroachDB

因此,我们可以灵活选择最适合项目需求的数据库,而不会牺牲 Prisma 带来的强大功能和性能。

这意味着你可以专注于真正重要的事情:构建你的应用逻辑。

Prisma 是 Elysia 的灵感来源之一,它声明式的 API 和流畅的开发者体验绝对是一大享受。

现在,随着 Bun 0.6.7 的发布,我们可以将期待已久的想象变为现实,Bun 现在开箱即用支持 Prisma。

Elysia

当你被问到“用 Bun 应该选择哪个框架”时,Elysia 是脑海中浮现的答案之一。

虽然你可以使用 Express 与 Bun 一起工作,但 Elysia 是专为 Bun 构建的。

Elysia 凭借声明式 API,可以比 Express 快近 19 倍,在创建统一类型系统和端到端类型安全方面表现出色。

Elysia 还以其流畅的开发者体验而闻名,尤其是 Elysia 从早期就设计为与 Prisma 一起使用。

借助 Elysia 的严格类型验证,我们可以使用声明式 API 轻松集成 Elysia 和 Prisma。

换句话说,Elysia 将确保运行时类型和 TypeScript 类型始终同步,让它表现得像严格类型语言,你可以完全信任类型系统,提前发现任何类型错误,并更容易调试与类型相关的错误。

设置

要开始,我们只需运行 bun create 来设置 Elysia 服务器

bash
bun create elysia elysia-prisma

其中 elysia-prisma 是我们的项目名称(文件夹目标),可以随意更改为任何你喜欢的名字。

现在进入我们的文件夹,并将 Prisma CLI 安装为开发依赖。

ts
bun add -d prisma

然后我们可以使用 prisma init 设置 Prisma 项目

ts
bunx prisma init

bunx 是 Bun 的命令,等同于 npx,它允许我们执行包的二进制文件。

设置完成后,我们会看到 Prisma 会更新 .env 文件并生成一个名为 prisma 的文件夹,里面有一个 schema.prisma 文件。

schema.prisma 是使用 Prisma 模式语言定义的数据库模型。

让我们像这样更新我们的 schema.prisma 文件作为演示:

ts
generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

model User {
  id        Int     @id @default(autoincrement())
  username  String  @unique
  password  String
}

告诉 Prisma 我们想要创建一个名为 User 的表,列如下:

类型约束
idNumber主键并自动递增
usernameString唯一
passwordString-

Prisma 将读取模式和 .env 文件中的 DATABASE_URL,因此在同步数据库之前,我们需要先定义 DATABASE_URL

由于我们没有运行任何数据库,我们可以使用 Docker 设置一个:

bash
docker run -p 5432:5432 -e POSTGRES_PASSWORD=12345678 -d postgres

现在进入项目根目录的 .env 文件并编辑:

DATABASE_URL="postgresql://postgres:12345678@localhost:5432/db?schema=public"

然后我们运行 prisma migrate 来将我们的数据库与 Prisma 模式同步:

bash
bunx prisma migrate dev --name init

Prisma 然后会基于我们的模式生成强类型 Prisma Client 代码。

这意味着我们在代码编辑器中获得自动补全和类型检查,在编译时捕获潜在错误,而不是运行时。

进入代码

在我们的 src/index.ts 中,让我们更新 Elysia 服务器来创建一个简单的用户注册端点。

ts
import { Elysia } from 'elysia'
import { PrismaClient } from '@prisma/client'

const db = new PrismaClient() 

const app = new Elysia()
    .post( 
        '/sign-up', 
        async ({ body }) => db.user.create({ 
            data: body 
        }) 
    ) 
    .listen(3000)

console.log(
    `🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}`
)

我们刚刚创建了一个简单的端点,使用 Elysia 和 Prisma 将新用户插入数据库。

TIP

重要提示:当返回 Prisma 函数时,你应该始终将回调函数标记为 async。

因为 Prisma 函数不返回原生 Promise,Elysia 无法动态处理自定义 Promise 类型,但通过静态代码分析,将回调函数标记为 async,Elysia 会尝试 await 函数的返回类型,从而允许我们映射 Prisma 结果。

现在问题是 body 可以是任何东西,不限于我们期望的定义类型。

我们可以通过使用 Elysia 的类型系统来改进这一点。

ts
import { Elysia, t } from 'elysia'
import { PrismaClient } from '@prisma/client'

const db = new PrismaClient()

const app = new Elysia()
    .post(
        '/sign-up',
        async ({ body }) => db.user.create({
            data: body
        }),
        { 
            body: t.Object({ 
                username: t.String(), 
                password: t.String({ 
                    minLength: 8
                }) 
            }) 
        } 
    )
    .listen(3000)

console.log(
    `🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}`
)

这告诉 Elysia 验证传入请求的 body 是否匹配形状,并更新回调中 body 的 TypeScript 类型以匹配完全相同的类型:

ts
// 'body' 现在被键入为以下类型:
{
    username: string
    password: string
}

这意味着如果形状与数据库表不兼容,它会立即警告你。

这在需要编辑表或执行迁移时非常有效,Elysia 可以在到达生产环境之前逐行记录类型冲突错误。

错误处理

由于我们的 username 字段是唯一的,有时 Prisma 可能会抛出错误,例如在注册时意外重复 username

ts
Invalid `prisma.user.create()` invocation:

Unique constraint failed on the fields: (`username`)

默认的 Elysia 错误处理器可以自动处理这种情况,但我们可以通过指定自定义错误使用 Elysia 的本地 onError 钩子来改进:

ts
import { Elysia, t } from 'elysia'
import { PrismaClient } from '@prisma/client'

const db = new PrismaClient()

const app = new Elysia()
    .post(
        '/',
        async ({ body }) => db.user.create({
            data: body
        }),
        {
            error({ code }) {  
                switch (code) {  
                    // Prisma P2002: "Unique constraint failed on the {constraint}"
                    case 'P2002':  
                        return {  
                            error: 'Username must be unique'
                        }  
                }  
            },  
            body: t.Object({
                username: t.String(),
                password: t.String({
                    minLength: 8
                })
            })
        }
    )
    .listen(3000)

console.log(
    `🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}`
)

使用 error 钩子,回调中抛出的任何错误都会填充到 error 钩子,允许我们定义自定义错误处理器。

根据 Prisma 文档,错误代码 'P2002' 表示执行查询时失败了唯一约束。

由于这个表只有一个唯一的 username 字段,我们可以推断错误是因为用户名不唯一,因此我们返回自定义错误消息:

ts
{
    error: 'Username must be unique'
}

当唯一约束失败时,这将返回我们自定义错误消息的 JSON 等价物。

允许我们从 Prisma 错误无缝定义任何自定义错误。

奖励:引用模式

当我们的服务器变得复杂,类型变得冗余并成为样板代码时,可以通过使用 Reference Schema 来改进内联 Elysia 类型。

简单来说,我们可以为模式命名并通过名称引用类型。

ts
import { Elysia, t } from 'elysia'
import { PrismaClient } from '@prisma/client'

const db = new PrismaClient()

const app = new Elysia()
    .model({ 
        'user.sign': t.Object({ 
            username: t.String(), 
            password: t.String({ 
                minLength: 8
            }) 
        }) 
    }) 
    .post(
        '/',
        async ({ body }) => db.user.create({
            data: body
        }),
        {
            error({ code }) {
                switch (code) {
                    // Prisma P2002: "Unique constraint failed on the {constraint}"
                    case 'P2002':
                        return {
                            error: 'Username must be unique'
                        }
                }
            },
            body: 'user.sign', 
            body: t.Object({ 
                username: t.String(), 
                password: t.String({ 
                    minLength: 8
                }) 
            }) 
        }
    )
    .listen(3000)

console.log(
    `🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}`
)

这与使用内联相同,但你只需定义一次,然后通过名称引用模式来移除冗余验证代码。

TypeScript 和验证代码将按预期工作。

奖励:文档

作为奖励,Elysia 类型系统还符合 OpenAPI Schema 3.0,这意味着它可以使用支持 OpenAPI Schema 的工具生成文档,例如 Swagger。

我们可以使用 Elysia Swagger 插件在一行代码中生成 API 文档。

bash
bun add @elysiajs/swagger

然后只需添加插件:

ts
import { Elysia, t } from 'elysia'
import { PrismaClient } from '@prisma/client'
import { swagger } from '@elysiajs/swagger'

const db = new PrismaClient()

const app = new Elysia()
    .use(swagger()) 
    .post(
        '/',
        async ({ body }) =>
            db.user.create({
                data: body,
                select: { 
                    id: true, 
                    username: true
                } 
            }),
        {
            error({ code }) {
                switch (code) {
                    // Prisma P2002: "Unique constraint failed on the {constraint}"
                    case 'P2002':
                        return {
                            error: 'Username must be unique'
                        }
                }
            },
            body: t.Object({
                username: t.String(),
                password: t.String({
                    minLength: 8
                })
            }),
            response: t.Object({ 
                id: t.Number(), 
                username: t.String() 
            }) 
        }
    )
    .listen(3000)

console.log(
    `🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}`
)

就这样,我们就可以为 API 创建一个定义良好的文档了。

由 Elysia 生成的 Swagger 文档

而且由于为文档定义了严格类型,我们发现我们意外地从 API 返回了 password 字段,这不是一个好主意,因为返回私有信息是不合适的。

感谢 Elysia 的类型系统,我们定义响应不应包含 password,这会自动警告我们 Prisma 查询返回了密码,让我们提前修复。

而且,如果有更多内容,我们不必担心可能会忘记 OpenAPI Schema 3.0 的规范,因为我们也有自动补全和类型安全。

我们可以使用 detail 定义路由细节,它也遵循 OpenAPI Schema 3.0,从而轻松创建文档。

接下来

借助 Bun 和 Elysia 对 Prisma 的支持,我们正在进入一个全新开发者体验时代。

对于 Prisma,我们可以加速与数据库的交互;Elysia 则在开发者体验和性能方面加速了后端 Web 服务器的创建。

这绝对是一种享受。

Elysia 正在努力为 Bun 创建一个新标准,以实现更好的开发者体验,构建高性能 TypeScript 服务器,其性能可以媲美 Go 和 Rust。

如果你想开始学习 Bun,不妨看看 Elysia 能提供什么,尤其是像 tRPC 那样的 端到端类型安全,但基于 REST 标准,没有任何代码生成。

如果你对 Elysia 感兴趣,欢迎查看我们的 Discord 服务器 或在 GitHub 上查看 Elysia

Elysia:人性化框架