用 Fastify 构建 Web API:从零到完整 CRUD
手把手用 Fastify + Prisma + SQLite 构建博客 API:Schema 校验、路由声明、ORM 集成与错误处理全流程,体验 Fastify 的现代 API 开发方式。
Fastify API 开发的标准结构:JSON Schema 声明在路由上(校验+文档一体)→ handler 内用 Prisma 操作数据库 → 错误由 setErrorHandler 统一兜底。相比 Express,校验无需另装中间件;响应序列化的编译优化需配置 response schema。
项目搭建
mkdir blog-api && cd blog-api
npm init -y
npm install fastify@5 @prisma/client@6
npm install -D prisma@6
npx prisma init --datasource-provider sqlite
Prisma 模型
在初始化生成的 schema.prisma 中追加以下模型,保留原有 generator 和 datasource 配置。
// prisma/schema.prisma
model Post {
id Int @id @default(autoincrement())
title String
content String
published Boolean @default(false)
createdAt DateTime @default(now())
}
npx prisma migrate dev --name init # 建表并生成客户端
Schema 驱动的路由
// 本文使用 Node.js 20+、Fastify 5 与 Prisma 6;Prisma 7 初始化方式不同
const fastify = require('fastify')({
logger: true,
ajv: { customOptions: { removeAdditional: false } },
});
const { PrismaClient } = require('@prisma/client');
const prisma = new PrismaClient();
const idSchema = {
type: 'object', required: ['id'],
properties: { id: { type: 'integer', minimum: 1 } },
};
const postBodySchema = {
type: 'object',
required: ['title', 'content'],
properties: {
title: { type: 'string', minLength: 1, maxLength: 200 },
content: { type: 'string', minLength: 1 },
published: { type: 'boolean' },
},
additionalProperties: false,
};
fastify.post('/posts', { schema: { body: postBodySchema } }, async (request) => {
const post = await prisma.post.create({ data: request.body });
return post; // 直接返回对象,Fastify 自动序列化
});
fastify.get('/posts', async () => {
return prisma.post.findMany({ orderBy: { createdAt: 'desc' } });
});
fastify.get('/posts/:id', { schema: { params: idSchema } }, async (request, reply) => {
const post = await prisma.post.findUnique({
where: { id: Number(request.params.id) },
});
if (!post) return reply.code(404).send({ message: '文章不存在' });
return post;
});
fastify.put('/posts/:id', { schema: { body: postBodySchema, params: idSchema } }, async (request, reply) => {
try {
return await prisma.post.update({
where: { id: Number(request.params.id) },
data: request.body,
});
} catch (error) {
if (error.code === 'P2025') {
return reply.code(404).send({ message: '文章不存在' });
}
throw error; // 连接失败等其他错误交给错误处理器,不能伪装成 404
}
});
fastify.delete('/posts/:id', { schema: { params: idSchema } }, async (request, reply) => {
try {
await prisma.post.delete({ where: { id: Number(request.params.id) } });
return reply.code(204).send();
} catch (error) {
if (error.code === 'P2025') {
return reply.code(404).send({ message: '文章不存在' });
}
throw error; // 连接失败等其他错误交给错误处理器,不能伪装成 404
}
});
fastify.listen({ port: 3000 });
Schema 校验失败的请求根本到不了 handler(自动 400);handler 内 return 即响应(只有声明匹配的 response schema 时才使用相应的编译序列化器,本例未声明响应 schema,不能据此断言性能或安全优势)。
Fastify 的独特优势在这个例子中尽显
- 校验零依赖:不用装 express-validator,Schema 即规则;
- 日志开箱:
logger: true内置 pino 结构化日志,每个请求自动带 ID; - async 原生:handler 就是 async 函数,异常自动进错误处理。
下一步
分页(cursor/offset)、认证(@fastify/jwt)、限流(@fastify/rate-limit)、OpenAPI 文档(@fastify/swagger 直接消费路由 Schema)——Fastify 官方插件全家桶一站配齐。
验证接口上线后的延迟与错误
用 Fastify 构建 Web API完成开发验证后,可选一条包含数据库或外部调用的业务路径接入观测云。按 Node.js APM 文档确认探针初始化顺序与框架兼容性,检查请求及下游 Span 是否连续,再按文档注入日志关联字段。以路由模板或稳定的操作名聚合,避免用用户 ID、动态 URL 或完整 GraphQL 查询作为高基数标签。
接入文档:Node.js APM 接入 · 日志采集 · 日志与链路关联。
常见问题(FAQ)
Q:request.body 在 Schema 之外还有字段会被注入吗?
本例显式设置 removeAdditional: false,结合 additionalProperties: false 才会拒绝多余字段;Fastify 默认配置通常会删除这些字段而不是报错。若没有设置 additionalProperties: false,多余字段可能保留;写库前用 pick 白名单字段更安全。
Q:Prisma 客户端在多文件间怎么共享?
写成 Fastify 插件(fastify-plugin 包装后 fastify.decorate('prisma', prisma)),路由里 fastify.prisma 访问,保证全应用单例。
Q:Fastify 和 Express 写法的主要心智差异?
三个:request/reply 替代 req/res;return data 替代 res.json(data)(reply.send 也可用);中间件概念变为插件+钩子。迁移详见本系列《从 Express 迁移到 Fastify》。
参考文档
本文依据公开文档整理,示例需按项目依赖版本验证;不将文档中的性能描述视为本项目实测结果。