adonisjs-best-practices by futuregerald/futuregerald-claude-plugin
npx skills add https://github.com/futuregerald/futuregerald-claude-plugin --skill adonisjs-best-practicesAdonisJS v6 是一个内置电池的 TypeScript 优先 MVC 框架。核心原则:类型安全、依赖注入和约定优于配置。
| 任务 | 模式 |
|---|---|
| 路由到控制器 | router.get('/users', [UsersController, 'index']) |
| 懒加载控制器 | const UsersController = () => import('#controllers/users_controller') |
| 验证请求 | const payload = await request.validateUsing(createUserValidator) |
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
| 身份验证检查 | await auth.authenticate() 或 auth.use('guard').authenticate() |
| 授权操作 | await bouncer.authorize('editPost', post) |
| 带关联的查询 | await User.query().preload('posts') |
app/
controllers/ # HTTP 处理器(精简,委托给服务)
models/ # Lucid ORM 模型
services/ # 业务逻辑
middleware/ # 请求拦截器
validators/ # VineJS 验证模式
exceptions/ # 自定义异常
policies/ # Bouncer 授权
start/
routes.ts # 路由定义
kernel.ts # 中间件注册
config/ # 配置文件
database/ # 迁移、种子、工厂
tests/ # 测试套件
懒加载控制器以支持 HMR 和更快的启动:
// start/routes.ts
const UsersController = () => import('#controllers/users_controller')
router.get('/users', [UsersController, 'index'])
router.post('/users', [UsersController, 'store'])
顺序很重要:在动态路由之前定义特定路由:
// 正确
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'])
使用路由组进行组织和批量中间件配置:
router
.group(() => {
router.resource('posts', PostsController)
router.resource('comments', CommentsController)
})
.prefix('/api/v1')
.middleware(middleware.auth())
资源控制器用于 RESTful CRUD:
router.resource('posts', PostsController)
// 创建:index, create, store, show, edit, update, destroy
命名路由用于 URL 生成:
router.get('/posts/:id', [PostsController, 'show']).as('posts.show')
// 使用: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(),
})
)
// 在控制器中
async store({ request }: HttpContext) {
const payload = await request.validateUsing(createPostValidator)
// payload 现在已类型化并已验证
}
数据库规则用于唯一性/存在性检查:
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.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'),
})
按路由应用:
router.get('/dashboard', [DashboardController, 'index']).middleware(middleware.auth())
根据客户端类型选择守卫:
会话守卫:服务器端渲染的应用(Web)
访问令牌:SPA/移动客户端(API)
// 基于会话(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') })
// 基于令牌(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'] }))
能力用于简单检查:
// 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) // 如果未授权则抛出异常
// 或者:if (await bouncer.allows('editPost', post)) { ... }
}
防止 N+1 使用预加载:
// 不好 - 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')
模型钩子用于业务逻辑:
// 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'
}
// 用法
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() 进行集成测试 |
每周安装次数
98
仓库
GitHub 星标数
3
首次出现
2026年2月7日
安全审计
安装于
codex93
gemini-cli93
opencode93
github-copilot92
kimi-cli90
amp90
AdonisJS v6 is a TypeScript-first MVC framework with batteries included. Core principle: type safety, dependency injection, and convention over configuration.
| Task | Pattern |
|---|---|
| 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') |
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
Lazy-load controllers for HMR support and faster boot:
// 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:
// 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:
router
.group(() => {
router.resource('posts', PostsController)
router.resource('comments', CommentsController)
})
.prefix('/api/v1')
.middleware(middleware.auth())
Resource controllers for RESTful CRUD:
router.resource('posts', PostsController)
// Creates: index, create, store, show, edit, update, destroy
Name routes for URL generation:
router.get('/posts/:id', [PostsController, 'show']).as('posts.show')
// Use: route('posts.show', { id: 1 })
Single responsibility : One controller per resource, thin handlers:
// 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:
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)
}
}
Validate immediately in controller, before any business logic:
// 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:
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' })),
})
)
Three stacks with distinct purposes:
// 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 :
router.get('/dashboard', [DashboardController, 'index']).middleware(middleware.auth())
Choose guard by client type :
Session guard : Server-rendered apps (web)
Access tokens : SPA/mobile clients (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() } })
Protect routes :
router
.group(() => {
router.get('/profile', [ProfileController, 'show'])
})
.middleware(middleware.auth({ guards: ['web'] }))
Abilities for simple checks:
// 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:
// 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 :
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)) { ... }
}
Prevent N+1 with eager loading:
// 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:
// 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:
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 })
})
Custom exceptions :
// 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 :
// 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 tests via test client:
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:
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)
})
})
| Mistake | Fix |
|---|---|
| Raw controller imports | Use lazy-loading: () => import('#controllers/...') |
| Validating in services | Validate in controller before business logic |
| N+1 queries | Use .preload() for eager loading |
| Dynamic route before specific | Order specific routes first |
| Skipping authorization | Always check permissions with Bouncer |
| Not using transactions | Wrap related operations in db.transaction() |
| Testing directly, not via HTTP | Use client.get() for integration tests |
Weekly Installs
98
Repository
GitHub Stars
3
First Seen
Feb 7, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
codex93
gemini-cli93
opencode93
github-copilot92
kimi-cli90
amp90
Node.js 环境配置指南:多环境管理、类型安全与最佳实践
10,500 周安装