如何使用 AdonisJS 构建 CRUD API
使用 AdonisJS v7 构建 CRUD API:posts 路由、Lucid 模型、VineJS 验证,以及用 curl 测试的 JSON 响应。
AdonisJS 是一个以 TypeScript 为优先、受 Laravel 启发的 Node.js 框架,集路由、ORM(Lucid)、验证(VineJS)、身份认证和 CLI(ace)于一体,无需额外配置即可使用——这使其非常适合构建 CRUD API,让开发者能够遵循约定而非手动接线。本教程将基于 AdonisJS v7 构建一个完整的 posts CRUD 端点,涵盖从脚手架搭建到 curl 请求测试的每个步骤,包括 VineJS 验证和结构化错误响应。
AdonisJS v7 与 v6 在本教程涉及的三个方面有所不同:环境配置(需要 Node.js 24+,使用 npm create adonisjs@latest);API 启动套件(采用 Turborepo monorepo 结构,后端位于 apps/backend/ 目录下,预配置了 Lucid ORM 和身份认证);以及代码模式(通过 #generated/controllers 进行路由绑定,Lucid 模型继承自 apps/backend/database/schema.ts 中生成的 schema 类)。以下示例全程遵循 v7 的项目结构。
核心要点
- 当前 AdonisJS v7 API 启动套件的脚手架命令为
npm create adonisjs@latest my-api -- --kit=api,而非旧版 v6 的npm init adonisjs@latest ... -- -K=api形式。 - AdonisJS v7 要求 Node.js 24+ 和 npm 11+,请在搭建项目前先升级本地运行时环境。
- 新版 v7 启动套件开箱即包含 Lucid ORM、SQLite、身份认证、VineJS 和测试配置,无需在全新的 API 启动套件中手动添加 Lucid。
- 当前 v7 路由示例使用生成的控制器桶文件导入方式:
import { controllers } from '#generated/controllers',然后以[controllers.Posts, 'index']风格绑定路由处理器。 - VineJS 对 API 请求的验证失败会返回 HTTP 422,响应体包含结构化的
errors数组,其中含有字段名、失败规则和错误消息——这正是前端表单库进行字段级错误展示所期望的数据格式。 - Lucid 的
Post.findOrFail(id)在记录不存在时会抛出异常,AdonisJS 会自动将其渲染为 404 JSON 响应,无需手动进行空值检查。
前置条件
AdonisJS v7 要求 Node.js 24 或更高版本,以及 npm 11 或更高版本。在搭建项目前,请先确认本地版本:
node --version # 必须 >= 24.x
npm --version # 必须 >= 11.x
v7 启动套件默认使用 SQLite 配置 Lucid,因此完成本教程无需从 PostgreSQL 开始。SQLite 将数据写入本地文件,非常适合用于一次性的 CRUD 演练。如果后续需要迁移到 PostgreSQL、MySQL 或其他 SQL 数据库,Lucid 同样支持这些驱动;下文中的控制器和验证代码无需任何改动。
使用 API 启动套件搭建项目
AdonisJS v7 的 API 启动套件通过 npm create adonisjs@latest <name> -- --kit=api 命令创建。现在运行以下命令:
npm create adonisjs@latest my-api -- --kit=api
--kit=api 标志用于选择 API 启动套件,它会创建一个以后端为中心的项目,具备 API 友好的默认配置,包括 Lucid ORM、VineJS 验证、身份认证、CORS、测试支持以及默认的 SQLite 数据库。
进入项目目录并启动开发服务器:
cd my-api
npm run dev
后端默认运行在 http://localhost:3333。API 启动套件还在 /api/v1/auth 下提供了身份认证端点,但本教程将 posts 端点保持为公开访问,以便优先专注于 CRUD 功能。
使用默认的 Lucid 配置
Discover how at OpenReplay.com.
Lucid 是 AdonisJS 的 SQL ORM。在较旧的教程中,你经常会看到如下手动安装步骤:
node ace add @adonisjs/lucid
请勿在全新的 v7 API 启动套件中运行该命令。Lucid 已经安装并配置完毕,且启动套件默认使用 SQLite。你可以直接跳到创建数据表的步骤。
生成迁移文件
迁移是对数据库 schema 的版本化变更。为 posts 表生成迁移文件:
node ace make:migration posts
该命令会在 database/migrations/ 目录下生成一个带时间戳的文件。打开该文件并定义表的列结构:
import { BaseSchema } from '@adonisjs/lucid/schema'
export default class extends BaseSchema {
protected tableName = 'posts'
async up() {
this.schema.createTable(this.tableName, (table) => {
table.increments('id')
table.string('title').notNullable()
table.text('body').notNullable()
table.timestamp('created_at')
table.timestamp('updated_at')
})
}
async down() {
this.schema.dropTable(this.tableName)
}
}
导入路径仍为 @adonisjs/lucid/schema。v7 的重要变化发生在迁移执行之后:Lucid 会根据你的数据库生成 schema 类,供应用模型继承并获得类型化的列定义。
执行迁移并创建模型
将 schema 应用到数据库:
node ace migration:run
Lucid 会创建 posts 表并生成 database/schema.ts。该生成文件包含一个 PostsSchema 类,其中包含该表的类型化列定义。请勿直接编辑 database/schema.ts,因为它会在每次迁移后重新生成。
现在创建模型:
node ace make:model Post
打开 app/models/post.ts,让模型继承从 database/schema.ts 导出的生成 schema 类:
import { PostSchema } from '#database/schema'
export default class Post extends PostSchema {}
这就是 v7 的 Lucid 模型模式。列装饰器存在于生成的 schema 类中,而你的应用模型则是后续添加关联关系、钩子、查询作用域和自定义方法的地方。
生成控制器
node ace make:controller posts --resource 命令会为 posts 资源搭建一个符合 REST 规范的控制器。现在运行:
node ace make:controller posts --resource
--resource 标志会生成常规的 REST 方法:index、create、store、show、edit、update 和 destroy。对于 JSON API,你只需要其中五个:
| 方法 | 用途 |
|---|---|
index | 列出所有文章 |
store | 根据请求体创建新文章 |
show | 根据 id 返回单篇文章 |
update | 根据 id 修改现有文章 |
destroy | 根据 id 删除文章 |
你可以删除生成的 create 和 edit 方法,因为它们是为服务端渲染的 HTML 表单设计的。
配置路由
当前 v7 文档展示了通过生成的桶文件 #generated/controllers 导入控制器的方式。打开 start/routes.ts 并添加 posts 路由:
import router from '@adonisjs/core/services/router'
import { controllers } from '#generated/controllers'
router
.group(() => {
router.get('/posts', [controllers.Posts, 'index'])
router.post('/posts', [controllers.Posts, 'store'])
router.get('/posts/:id', [controllers.Posts, 'show'])
router.route('/posts/:id', ['PUT', 'PATCH'], [controllers.Posts, 'update'])
router.delete('/posts/:id', [controllers.Posts, 'destroy'])
})
.prefix('/api/v1')
这将在 /api/v1/posts 下注册 API 端点,与 v7 API 启动套件采用的版本化 API 风格保持一致。
| HTTP 方法 | 路径 | 控制器方法 | 描述 |
|---|---|---|---|
| GET | /api/v1/posts | index | 列出所有文章 |
| POST | /api/v1/posts | store | 创建文章 |
| GET | /api/v1/posts/:id | show | 获取单篇文章 |
| PUT | /api/v1/posts/:id | update | 更新文章 |
| PATCH | /api/v1/posts/:id | update | 部分更新文章 |
| DELETE | /api/v1/posts/:id | destroy | 删除文章 |
AdonisJS 仍然在文档中介绍了 router.resource() 用于 RESTful 路由,但普通的资源路由会注册七条路由,包括 GET /posts/create 和 GET /posts/:id/edit。上面显式列出的路由列表使 API 接口仅限 JSON,避免意外暴露表单显示路由。
使用以下命令验证当前已注册的路由:
node ace list:routes
如果路由未出现,请重启开发服务器以刷新生成的控制器桶文件。
添加 VineJS 验证
VineJS 是 AdonisJS 使用的验证库。生成一个验证器文件:
node ace make:validator post
打开 app/validators/post.ts,为创建和更新文章定义验证器:
import vine from '@vinejs/vine'
export const createPostValidator = vine.create({
title: vine.string().trim().minLength(3),
body: vine.string().trim().minLength(1),
})
export const updatePostValidator = vine.create({
title: vine.string().trim().minLength(3).optional(),
body: vine.string().trim().minLength(1).optional(),
})
当前 AdonisJS 验证指南使用 vine.create() 来定义验证器。在控制器中,request.validateUsing() 会对请求体执行验证,并返回经过类型化处理的验证后数据。
当 request.validateUsing() 对 API 请求验证失败时,AdonisJS 会返回 HTTP 422,响应体包含结构化的 JSON errors 数组。每个错误条目包含字段名、失败规则和错误消息,前端代码可以直接将其映射到表单字段状态。
在对消费 CRUD API 的前端进行会话回放时,经常会发现一个特定的故障模式:表单提交后,服务器返回 422,但 UI 中始终不显示字段级错误消息。根本原因通常是服务器返回了自由格式的字符串,而非结构化的字段错误。VineJS 默认提供结构化的错误格式,请务必保留这一特性。除非同时保留字段级契约,否则不要捕获验证异常后将其重新抛出为扁平化字符串。
实现控制器方法
每个控制器动作都是一个简短的处理函数。两种需要处理的失败场景——记录不存在和输入无效——分别由 Lucid 和 VineJS 自动处理,无需手动编写条件分支。
打开 app/controllers/posts_controller.ts,将其内容替换为:
import type { HttpContext } from '@adonisjs/core/http'
import Post from '#models/post'
import { createPostValidator, updatePostValidator } from '#validators/post'
export default class PostsController {
async index({ response }: HttpContext) {
const posts = await Post.all()
return response.json(posts)
}
async store({ request, response }: HttpContext) {
const payload = await request.validateUsing(createPostValidator)
const post = await Post.create(payload)
return response.created(post)
}
async show({ params, response }: HttpContext) {
const post = await Post.findOrFail(params.id)
return response.json(post)
}
async update({ params, request, response }: HttpContext) {
const post = await Post.findOrFail(params.id)
const payload = await request.validateUsing(updatePostValidator)
await post.merge(payload).save()
return response.json(post)
}
async destroy({ params, response }: HttpContext) {
const post = await Post.findOrFail(params.id)
await post.delete()
return response.noContent()
}
}
以下几个细节值得注意:
store返回response.created(post),对应 HTTP 201 状态码。destroy返回response.noContent(),对应 HTTP 204 状态码。await post.delete()使用了await,确保响应不会在删除操作完成前返回。Post.findOrFail(params.id)在没有匹配行时抛出异常,AdonisJS 会自动将该异常转换为 404 响应。
使用 curl 进行测试
AdonisJS v7 的 CRUD API 可以通过 curl 进行端到端测试——每个资源操作(index、store、show、update、destroy)都对应 /api/v1/posts 下的一个 HTTP 请求。在 npm run dev 运行的情况下,创建一篇文章:
curl -X POST http://localhost:3333/api/v1/posts \
-H "Content-Type: application/json" \
-d '{"title": "First post", "body": "Hello from Adonis"}'
成功的 store 响应返回 HTTP 201 以及已创建的记录:
{
"title": "First post",
"body": "Hello from Adonis",
"createdAt": "2026-06-01T12:00:00.000+00:00",
"updatedAt": "2026-06-01T12:00:00.000+00:00",
"id": 1
}
现在通过发送过短的标题和空的正文来触发验证错误:
curl -X POST http://localhost:3333/api/v1/posts \
-H "Content-Type: application/json" \
-d '{"title": "ok", "body": ""}'
响应为 HTTP 422,包含结构化的 VineJS 错误数组:
{
"errors": [
{
"field": "title",
"rule": "minLength",
"message": "The title field must have at least 3 characters"
},
{
"field": "body",
"rule": "minLength",
"message": "The body field must have at least 1 characters"
}
]
}
这就是前端映射到表单状态的数据契约:遍历 errors 数组,以 field 为键,将 message 渲染在对应输入框旁边。
获取一条不存在的记录,以观察 404 响应路径:
curl -i http://localhost:3333/api/v1/posts/9999
findOrFail 会产生 404 响应,而不是返回 null 并附带具有误导性的 200 状态码。
其余 CRUD 请求遵循相同的模式:
curl http://localhost:3333/api/v1/posts # index
curl http://localhost:3333/api/v1/posts/1 # show
curl -X PUT http://localhost:3333/api/v1/posts/1 \
-H "Content-Type: application/json" \
-d '{"title": "Updated title"}' # update
curl -X PATCH http://localhost:3333/api/v1/posts/1 \
-H "Content-Type: application/json" \
-d '{"body": "Updated body"}' # 部分更新
curl -X DELETE http://localhost:3333/api/v1/posts/1 # destroy → 204
后续步骤
AdonisJS v7 的 CRUD API 在投入生产之前还需要完善三个方面:身份认证、分页和测试。
身份认证方面,API 启动套件已经包含了认证路由和基于访问令牌的配置。当 posts 路由需要仅对已登录用户开放时,使用 auth 中间件对其进行保护。如果你是在现有的非启动套件项目中添加认证,文档中的命令为:
node ace add @adonisjs/auth --guard=access_tokens
分页方面,对于大型数据集,将 index 中的 Post.all() 替换为 Lucid 的分页查询,使端点每次只返回一页数据,而不是整张表的内容。
测试方面,使用启动套件配置的测试运行器 Japa。v7 API 启动套件的测试命令为:
npm run test
优先测试对前端最重要的三个契约:成功创建返回 201、验证失败返回 422 并包含字段级错误、记录不存在返回 404。
常见问题
为什么本 v7 教程使用显式路由而非 router.resource().apiOnly()?
当前 AdonisJS v7 路由文档重点介绍了 router.resource(),但普通的资源路由会注册七条路由,包括用于 HTML 表单的 GET /posts/create 和 GET /posts/:id/edit。对于纯 JSON API,显式列出 GET、POST、PUT、PATCH 和 DELETE 路由能让 API 接口一目了然,同时避免意外注册表单显示路由。
为什么 node ace make:controller --resource 会生成七个方法,而本 API 只使用五个?
--resource 标志会生成完整的 RESTful 控制器方法集:index、create、store、show、edit、update 和 destroy。create 和 edit 方法用于在服务端渲染应用中渲染 HTML 表单。JSON API 只需要 index、store、show、update 和 destroy,因此可以安全地删除未使用的 create 和 edit 方法。
update 端点应该支持哪些 HTTP 方法?
本教程将 /api/v1/posts/:id 上的 PUT 和 PATCH 都映射到同一个 update 控制器方法。PUT 通常表示对资源的完整替换,PATCH 表示部分更新,但许多 CRUD API 通过相同的验证和合并逻辑处理这两种情况。运行 node ace list:routes 可以确认应用中的实际路由映射。
AdonisJS v6 的教程在 v7 中还适用吗?
不能可靠地适用。控制器逻辑的大部分看起来仍然相似,但 v7 在一些重要的教程细节上有所变化:运行时要求为 Node.js 24+,脚手架使用 npm create adonisjs@latest,启动套件默认包含 Lucid 和认证,当前路由示例使用 #generated/controllers,Lucid 模型继承自 #database/schema 中生成的 schema 类。请将 v6 教程仅作为概念性参考。
Gain Debugging Superpowers
Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers.
Star on GitHub12k