adonisjs-best-practices
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseAdonisJS v6 Best Practices
AdonisJS v6 最佳实践
Overview
概述
AdonisJS v6 is a TypeScript-first MVC framework with batteries included. Core principle: type safety, dependency injection, and convention over configuration.
AdonisJS v6 是一个优先支持TypeScript的全功能MVC框架。核心原则:类型安全、依赖注入、约定优于配置。
When to Use
适用场景
- Building new AdonisJS v6 features
- Implementing routes, controllers, middleware
- Setting up authentication or authorization
- Writing Lucid ORM models and queries
- Creating validators with VineJS
- Writing tests for AdonisJS apps
- 构建新的AdonisJS v6功能
- 实现路由、控制器、中间件
- 配置认证或授权
- 编写Lucid ORM模型和查询
- 使用VineJS创建验证器
- 为AdonisJS应用编写测试
Quick Reference
快速参考
| Task | Pattern |
|---|---|
| Route to controller | |
| Lazy-load controller | |
| Validate request | |
| Auth check | |
| Authorize action | |
| Query with relations | |
| 任务 | 实现模式 |
|---|---|
| 路由绑定控制器 | |
| 懒加载控制器 | |
| 请求验证 | |
| 认证检查 | |
| 操作授权 | |
| 关联查询 | |
Project Structure
项目结构
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 suitesapp/
controllers/ # HTTP处理器(精简,将业务逻辑委托给服务)
models/ # Lucid ORM模型
services/ # 业务逻辑层
middleware/ # 请求拦截器
validators/ # VineJS验证规则
exceptions/ # 自定义异常
policies/ # Bouncer授权策略
start/
routes.ts # 路由定义
kernel.ts # 中间件注册
config/ # 配置文件
database/ # 迁移文件、数据填充、工厂类
tests/ # 测试套件Routing
路由
Lazy-load controllers for HMR support and faster boot:
typescript
// start/routes.ts
const UsersController = () => import('#controllers/users_controller')
router.get('/users', [UsersController, 'index'])
router.post('/users', [UsersController, 'store'])Order matters: Define specific routes before dynamic ones:
typescript
// 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'])Use route groups for organization and bulk middleware:
typescript
router
.group(() => {
router.resource('posts', PostsController)
router.resource('comments', CommentsController)
})
.prefix('/api/v1')
.middleware(middleware.auth())Resource controllers for RESTful CRUD:
typescript
router.resource('posts', PostsController)
// Creates: index, create, store, show, edit, update, destroyName routes for URL generation:
typescript
router.get('/posts/:id', [PostsController, 'show']).as('posts.show')
// Use: route('posts.show', { id: 1 })懒加载控制器以支持HMR(热模块替换)并加快启动速度:
typescript
// start/routes.ts
const UsersController = () => import('#controllers/users_controller')
router.get('/users', [UsersController, 'index'])
router.post('/users', [UsersController, 'store'])路由顺序很重要:先定义具体路由,再定义动态路由:
typescript
// 正确写法
router.get('/users/me', [UsersController, 'me'])
router.get('/users/:id', [UsersController, 'show'])
// 错误写法 - /users/me 将永远无法匹配
router.get('/users/:id', [UsersController, 'show'])
router.get('/users/me', [UsersController, 'me'])使用路由分组进行组织和批量应用中间件:
typescript
router
.group(() => {
router.resource('posts', PostsController)
router.resource('comments', CommentsController)
})
.prefix('/api/v1')
.middleware(middleware.auth())资源控制器用于RESTful CRUD操作:
typescript
router.resource('posts', PostsController)
// 自动生成:index, create, store, show, edit, update, destroy 方法为路由命名以方便生成URL:
typescript
router.get('/posts/:id', [PostsController, 'show']).as('posts.show')
// 使用方式:route('posts.show', { id: 1 })Controllers
控制器
Single responsibility: One controller per resource, thin handlers:
typescript
// 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)
}
}Method injection for services:
typescript
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)
}
}单一职责原则:一个控制器对应一个资源,处理器逻辑精简:
typescript
// 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)
}
}方法注入获取服务实例:
typescript
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)
}
}Validation
验证
Validate immediately in controller, before any business logic:
typescript
// 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
}Database rules for unique/exists checks:
typescript
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' })),
})
)在控制器中立即验证,在执行任何业务逻辑之前:
typescript
// 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(),
})
)
// 在控制器中使用
async store({ request }: HttpContext) {
const payload = await request.validateUsing(createPostValidator)
// payload 现在是经过类型校验和验证的
}数据库规则用于唯一性/存在性检查:
typescript
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' })),
})
)Middleware
中间件
Three stacks with distinct purposes:
typescript
// 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'),
})Apply per-route:
typescript
router.get('/dashboard', [DashboardController, 'index']).middleware(middleware.auth())三种中间件栈,各有不同用途:
typescript
// start/kernel.ts
// 服务器中间件:处理所有请求(静态文件、健康检查等)
server.use([() => import('#middleware/container_bindings_middleware')])
// 路由中间件:仅处理匹配到的路由(认证、日志等)
router.use([() => import('@adonisjs/cors/cors_middleware')])
// 命名中间件:显式分配使用
export const middleware = router.named({
auth: () => import('#middleware/auth_middleware'),
guest: () => import('#middleware/guest_middleware'),
})为单个路由应用中间件:
typescript
router.get('/dashboard', [DashboardController, 'index']).middleware(middleware.auth())Authentication
认证
Choose guard by client type:
- Session guard: Server-rendered apps (web)
- Access tokens: SPA/mobile clients (api)
typescript
// 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() }
})Protect routes:
typescript
router
.group(() => {
router.get('/profile', [ProfileController, 'show'])
})
.middleware(middleware.auth({ guards: ['web'] }))根据客户端类型选择认证守卫:
- Session守卫:服务端渲染应用(网页)
- 访问令牌:SPA/移动客户端(API)
typescript
// 基于Session的认证(网页)
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')
})
// 基于令牌的认证(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() }
})保护路由:
typescript
router
.group(() => {
router.get('/profile', [ProfileController, 'show'])
})
.middleware(middleware.auth({ guards: ['web'] }))Authorization (Bouncer)
授权(Bouncer)
Abilities for simple checks:
typescript
// 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
})Policies for resource-based authorization:
typescript
// 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
}
}Use in controllers:
typescript
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)) { ... }
}**能力(Abilities)**用于简单检查:
typescript
// 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
})**策略(Policies)**用于基于资源的授权:
typescript
// 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
}
}在控制器中使用:
typescript
async update({ bouncer, params, request }: HttpContext) {
const post = await Post.findOrFail(params.id)
await bouncer.authorize('editPost', post) // 未授权时抛出异常
// 或者:if (await bouncer.allows('editPost', post)) { ... }
}Database (Lucid ORM)
数据库(Lucid ORM)
Prevent N+1 with eager loading:
typescript
// 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')Model hooks for business logic:
typescript
// 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)
}
}
}Transactions for atomic operations:
typescript
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 })
})使用预加载避免N+1查询:
typescript
// 错误写法 - N+1次查询
const posts = await Post.all()
for (const post of posts) {
console.log(post.author.name) // 每个帖子都会触发一次查询
}
// 正确写法 - 总共2次查询
const posts = await Post.query().preload('author')模型钩子用于处理业务逻辑:
typescript
// 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)
}
}
}事务用于原子操作:
typescript
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 })
})Error Handling
错误处理
Custom exceptions:
typescript
// 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')Global exception handler:
typescript
// 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)
}
}自定义异常:
typescript
// 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'
}
// 使用方式
throw new NotFoundException('Post not found')全局异常处理器:
typescript
// 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)
}
}Testing
测试
HTTP tests via test client:
typescript
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)
})
})Database isolation with transactions:
typescript
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)
})
})通过测试客户端编写HTTP测试:
typescript
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)
})
})使用事务实现数据库隔离:
typescript
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)
})
})Common Mistakes
常见错误
| Mistake | Fix |
|---|---|
| Raw controller imports | Use lazy-loading: |
| Validating in services | Validate in controller before business logic |
| N+1 queries | Use |
| Dynamic route before specific | Order specific routes first |
| Skipping authorization | Always check permissions with Bouncer |
| Not using transactions | Wrap related operations in |
| Testing directly, not via HTTP | Use |
| 错误类型 | 修复方案 |
|---|---|
| 直接导入控制器 | 使用懒加载: |
| 在服务层中验证请求 | 在控制器中先验证,再执行业务逻辑 |
| N+1查询问题 | 使用 |
| 动态路由放在具体路由之前 | 先定义具体路由,再定义动态路由 |
| 跳过授权检查 | 始终使用Bouncer检查权限 |
| 不使用事务 | 将相关操作包裹在 |
| 直接测试逻辑而非通过HTTP | 集成测试使用 |