Hapi.js 构建 Web API 实战:博客 CRUD 全流程
Hapi.js 以配置驱动与内建安全著称。本文用 Hapi + Mongoose 构建完整博客 API:模型定义、校验 Schema 与增删改查全流程。
Hapi 的路由把校验内建进定义:options: { validate: { payload: Joi.object(...) } }——非法请求自动 400。配 Mongoose 操作 MongoDB,CRUD 五个路由即可完成生产级博客 API 骨架。
项目搭建
npm install @hapi/hapi mongoose joi
const Hapi = require('@hapi/hapi');
const mongoose = require('mongoose');
await mongoose.connect(process.env.MONGO_URL);
const Post = mongoose.model('Post', new mongoose.Schema({
title: { type: String, required: true },
content: { type: String, required: true },
published: { type: Boolean, default: false },
}, { timestamps: true }));
const server = Hapi.server({ port: 3000, host: 'localhost' });
校验 Schema(Joi)
const postSchema = Joi.object({
title: Joi.string().min(1).max(200).required(),
content: Joi.string().min(1).required(),
published: Joi.boolean(),
});
CRUD 路由
// 创建
server.route({
method: 'POST',
path: '/posts',
options: {
validate: { payload: postSchema, failAction: (req, h, err) => { throw err; } },
},
handler: async (req) => {
const post = await Post.create(req.payload);
return { data: post };
},
});
// 列表
server.route({
method: 'GET',
path: '/posts',
handler: async () => ({ data: await Post.find().sort('-createdAt').limit(50) }),
});
// 详情
server.route({
method: 'GET',
path: '/posts/{id}',
handler: async (req, h) => {
const post = await Post.findById(req.params.id);
if (!post) return h.response({ error: '不存在' }).code(404);
return { data: post };
},
});
// 更新与删除同理:PUT /posts/{id}(validate payload)、DELETE /posts/{id}
Hapi 的差异化体验
- validate 内建:payload/params/query 校验声明在路由配置中——不过校验的请求根本到不了 handler;
- Boom 错误:
Boom.notFound()语义化错误响应; - 生命周期钩子:认证/日志在请求生命周期扩展点插入。
验证接口上线后的延迟与错误
Hapi.js 构建 Web API 实战完成开发验证后,可选一条包含数据库或外部调用的业务路径接入观测云。按 Node.js APM 文档确认探针初始化顺序与框架兼容性,检查请求及下游 Span 是否连续,再按文档注入日志关联字段。以路由模板或稳定的操作名聚合,避免用用户 ID、动态 URL 或完整 GraphQL 查询作为高基数标签。
接入文档:Node.js APM 接入 · 日志采集 · 日志与链路关联。
常见问题(FAQ)
Q:Hapi 和 Express 怎么选?
新项目建议先看 Fastify/NestJS(生态更活跃);Hapi 的"配置驱动 + 内建安全"理念仍有学习价值。存量 Hapi 项目本文即实战参考。
Q:Mongoose 的校验和 Joi 重复吗?
职责不同:Joi 守 HTTP 边界(请求形状),Mongoose 守数据边界(入库约束)——两层都保留。
Q:分页怎么做?
Post.find().skip((page-1)*size).limit(size) + total 计数;大数据量用游标(_id 范围)分页。
参考文档
本文依据公开文档整理,示例需按项目依赖版本验证;不将文档中的性能描述视为本项目实测结果。