adonisjs-best-practices

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

AdonisJS 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

快速参考

TaskPattern
Route to controller
router.get('/users', [UsersController, 'index'])
Lazy-load controller
const UsersController = () => import('#controllers/users_controller')
Validate request
const payload = await request.validateUsing(createUserValidator)
Auth check
await auth.authenticate()
or
auth.use('guard').authenticate()
Authorize action
await bouncer.authorize('editPost', post)
Query with relations
await User.query().preload('posts')
任务实现模式
路由绑定控制器
router.get('/users', [UsersController, 'index'])
懒加载控制器
const UsersController = () => import('#controllers/users_controller')
请求验证
const payload = await request.validateUsing(createUserValidator)
认证检查
await auth.authenticate()
or
auth.use('guard').authenticate()
操作授权
await bouncer.authorize('editPost', post)
关联查询
await User.query().preload('posts')

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 suites
app/
  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, destroy
Name 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

常见错误

MistakeFix
Raw controller importsUse lazy-loading:
() => import('#controllers/...')
Validating in servicesValidate in controller before business logic
N+1 queriesUse
.preload()
for eager loading
Dynamic route before specificOrder specific routes first
Skipping authorizationAlways check permissions with Bouncer
Not using transactionsWrap related operations in
db.transaction()
Testing directly, not via HTTPUse
client.get()
for integration tests
错误类型修复方案
直接导入控制器使用懒加载:
() => import('#controllers/...')
在服务层中验证请求在控制器中先验证,再执行业务逻辑
N+1查询问题使用
.preload()
进行预加载
动态路由放在具体路由之前先定义具体路由,再定义动态路由
跳过授权检查始终使用Bouncer检查权限
不使用事务将相关操作包裹在
db.transaction()
直接测试逻辑而非通过HTTP集成测试使用
client.get()
等方法