关于 adonisjs-best-practices
adonisjs-best-practices 是面向 AdonisJS v6 开发者的实践参考技能,涵盖路由、验证、身份验证、数据库访问、授权和测试等常见模式。
主要功能
- 01路由与控制器模式
- 02使用 VineJS 验证请求
- 03身份验证与 Bouncer 授权
- 04Lucid ORM 查询与事务
- 05HTTP 测试与数据库隔离
适用场景
- 01实现路由和控制器
- 02添加身份验证与访问权限检查
- 03构建经过验证的数据库功能
- 04编写隔离的 HTTP 集成测试
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ futuregerald/ futuregerald-claude-plugin --skill adonisjs-best-practices安装后在 Claude Code 中输入/adonisjs-best-practices确认已加载。
详细介绍
AdonisJS v6 最佳实践
概述
AdonisJS v6 是一个内置功能齐全、以 TypeScript 为先的 MVC 框架。核心原则:类型安全、依赖注入,以及约定优于配置。
适用场景
- 构建新的 AdonisJS v6 功能
- 实现路由、控制器和中间件
- 设置身份验证或授权
- 编写 Lucid ORM 模型和查询
- 使用 VineJS 创建验证器
- 为 AdonisJS 应用编写测试
快速参考
| 任务 | 模式 |
|---|---|
| 路由到控制器 | router.get('/users', [UsersController, 'index']) |
| 延迟加载控制器 | const UsersController = () => import('#controllers/users_controller') |
| 验证请求 | const payload = await request.validateUsing(createUserValidator) |
| 身份验证检查 | await auth.authenticate() 或 auth.use('guard').authenticate() |
| 授权操作 | await bouncer.authorize('editPost', post) |
| 查询关联关系 | await User.query().preload('posts') |
项目结构
app/
controllers/ # HTTP handlers (thin, delegate to services)
models/ # Lucid ORM models
services/ # Business logic
middleware/ # Request interceptors
validators/ # VineJS validation schemas
exceptions/ # Custom exceptions
policies/ # Bouncer authorization
start/
routes.ts # Route definitions
kernel.ts # Middleware registration
config/ # Configuration files
database/ # Migrations, seeders, factories
tests/ # Test suites
路由
延迟加载控制器,以支持 HMR 并加快启动速度:
// start/routes.ts
const UsersController = () => import('#controllers/users_controller')
router.get('/users', [UsersController, 'index'])
router.post('/users', [UsersController, 'store'])
顺序很重要:先定义具体路由,再定义动态路由:
// CORRECT
router.get('/users/me', [UsersController, 'me'])
router.get('/users/:id', [UsersController, 'show'])
// WRONG - /users/me will never match
router.get('/users/:id', [UsersController, 'show'])
router.get('/users/me', [UsersController, 'me'])
使用路由组进行组织并批量应用中间件:
router
.group(() => {
router.resource('posts', PostsController)
router.resource('comments', CommentsController)
})
.prefix('/api/v1')
.middleware(middleware.auth())
资源控制器用于 RESTful CRUD:
router.resource('posts', PostsController)
// Creates: index, create, store, show, edit, update, destroy
为路由命名以生成 URL:
router.get('/posts/:id', [PostsController, 'show']).as('posts.show')
// Use: route('posts.show', { id: 1 })
控制器
单一职责:每种资源对应一个控制器,处理程序保持精简:
// app/controllers/posts_controller.ts
export default class PostsController {
async index({ request, response }: HttpContext) {
const posts = await Post.query().preload('author')
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)
}
}
方法注入服务:
import { inject } from '@adonisjs/core'
import PostService from '#services/post_service'
export default class PostsController {
@inject()
async store({ request }: HttpContext, postService: PostService) {
const payload = await request.validateUsing(createPostValidator)
return postService.create(payload)
}
}
验证
在控制器中立即验证,并在任何业务逻辑执行之前完成:
// app/validators/post_validator.ts
import vine from '@vinejs/vine'
export const createPostValidator = vine.compile(
vine.object({
title: vine.string().trim().minLength(3).maxLength(255),
content: vine.string().trim(),
published: vine.boolean().optional(),
})
)
// In controller
async store({ request }: HttpContext) {
const payload = await request.validateUsing(createPostValidator)
// payload is now typed and validated
}
数据库规则用于检查唯一性或是否存在:
import vine from '@vinejs/vine'
import { uniqueRule } from '#validators/rules/unique'
export const createUserValidator = vine.compile(
vine.object({
email: vine
.string()
.email()
.use(uniqueRule({ table: 'users', column: 'email' })),
})
)
中间件
三个中间件栈,用途各不相同:
// start/kernel.ts
// Server middleware: ALL requests (static files, health checks)
server.use([() => import('#middleware/container_bindings_middleware')])
// Router middleware: matched routes only (auth, logging)
router.use([() => import('@adonisjs/cors/cors_middleware')])
// Named middleware: explicit assignment
export const middleware = router.named({
auth: () => import('#middleware/auth_middleware'),
guest: () => import('#middleware/guest_middleware'),
})
按路由应用:
router.get('/dashboard', [DashboardController, 'index']).middleware(middleware.auth())
身份验证
根据客户端类型选择 guard:
- 会话 guard:服务端渲染的应用(web)
- 访问令牌:SPA/移动客户端(api)
// Session-based (web)
router.post('/login', async ({ auth, request, response }) => {
const { email, password } = await request.validateUsing(loginValidator)
const user = await User.verifyCredentials(email, password)
await auth.use('web').login(user)
return response.redirect('/dashboard')
})
// Token-based (API)
router.post('/api/login', async ({ request }) => {
const { email, password } = await request.validateUsing(loginValidator)
const user = await User.verifyCredentials(email, password)
const token = await User.accessTokens.create(user)
return { token: token.value!.release() }
})
保护路由:
router
.group(() => {
router.get('/profile', [ProfileController, 'show'])
})
.middleware(middleware.auth({ guards: ['web'] }))
授权(Bouncer)
简单检查使用能力:
// app/abilities/main.ts
import { Bouncer } from '@adonisjs/bouncer'
import User from '#models/user'
import Post from '#models/post'
export const editPost = Bouncer.ability((user: User, post: Post) => {
return user.id === post.userId
})
基于资源的授权使用策略:
// app/policies/post_policy.ts
import { BasePolicy } from '@adonisjs/bouncer'
import User from '#models/user'
import Post from '#models/post'
export default class PostPolicy extends BasePolicy {
edit(user: User, post: Post) {
return user.id === post.userId
}
delete(user: User, post: Post) {
return user.id === post.userId || user.isAdmin
}
}
在控制器中使用:
async update({ bouncer, params, request }: HttpContext) {
const post = await Post.findOrFail(params.id)
await bouncer.authorize('editPost', post) // Throws if unauthorized
// or: if (await bouncer.allows('editPost', post)) { ... }
}
数据库(Lucid ORM)
使用预加载避免 N+1 查询:
// BAD - N+1 queries
const posts = await Post.all()
for (const post of posts) {
console.log(post.author.name) // Query per post
}
// GOOD - 2 queries total
const posts = await Post.query().preload('author')
使用模型钩子处理业务逻辑:
// app/models/user.ts
import { beforeSave, column } from '@adonisjs/lucid/orm'
import hash from '@adonisjs/core/services/hash'
export default class User extends BaseModel {
@column()
declare password: string
@beforeSave()
static async hashPassword(user: User) {
if (user.$dirty.password) {
user.password = await hash.make(user.password)
}
}
}
使用事务执行原子操作:
import db from '@adonisjs/lucid/services/db'
await db.transaction(async (trx) => {
const user = await User.create({ email }, { client: trx })
await Profile.create({ userId: user.id }, { client: trx })
})
错误处理
自定义异常:
// app/exceptions/not_found_exception.ts
import { Exception } from '@adonisjs/core/exceptions'
export default class NotFoundException extends Exception {
static status = 404
static code = 'E_NOT_FOUND'
}
// Usage
throw new NotFoundException('Post not found')
全局异常处理器:
// app/exceptions/handler.ts
import { ExceptionHandler, HttpContext } from '@adonisjs/core/http'
export default class HttpExceptionHandler extends ExceptionHandler {
async handle(error: unknown, ctx: HttpContext) {
if (error instanceof NotFoundException) {
return ctx.response.status(404).json({ error: error.message })
}
return super.handle(error, ctx)
}
}
测试
通过测试客户端进行 HTTP 测试:
import { test } from '@japa/runner'
test.group('Posts', () => {
test('can list posts', async ({ client }) => {
const response = await client.get('/api/posts')
response.assertStatus(200)
response.assertBodyContains({ data: [] })
})
test('requires auth to create post', async ({ client }) => {
const response = await client.post('/api/posts').json({ title: 'Test' })
response.assertStatus(401)
})
test('authenticated user can create post', async ({ client }) => {
const user = await UserFactory.create()
const response = await client
.post('/api/posts')
.loginAs(user)
.json({ title: 'Test', content: 'Content' })
response.assertStatus(201)
})
})
使用事务实现数据库隔离:
import { test } from '@japa/runner'
import testUtils from '@adonisjs/core/services/test_utils'
test.group('Posts', (group) => {
group.each.setup(() => testUtils.db().withGlobalTransaction())
test('creates post in database', async ({ client, assert }) => {
const user = await UserFactory.create()
await client.post('/api/posts').loginAs(user).json({ title: 'Test' })
const post = await Post.findBy('title', 'Test')
assert.isNotNull(post)
})
})
常见错误
| 错误 | 修正 |
|---|---|
| 直接导入控制器 | 使用延迟加载:() => import('#controllers/...') |
| 在服务中验证 | 在业务逻辑执行前于控制器中验证 |
| N+1 查询 | 使用 .preload() 进行预加载 |
| 动态路由早于具体路由 | 先定义具体路由 |
| 跳过授权 | 始终使用 Bouncer 检查权限 |
| 不使用事务 | 使用 db.transaction() 包裹相关操作 |
| 直接测试,而非通过 HTTP | 集成测试使用 client.get() |
评论
支持 Markdown