mcpField
A
adonisjs-best-practices

adonisjs-best-practices

github.com/futuregerald开发7216 次安装今天更新
代码仓库

关于 adonisjs-best-practices

adonisjs-best-practices 是面向 AdonisJS v6 开发者的实践参考技能,涵盖路由、验证、身份验证、数据库访问、授权和测试等常见模式。

主要功能

  1. 01路由与控制器模式
  2. 02使用 VineJS 验证请求
  3. 03身份验证与 Bouncer 授权
  4. 04Lucid ORM 查询与事务
  5. 05HTTP 测试与数据库隔离

适用场景

  1. 01实现路由和控制器
  2. 02添加身份验证与访问权限检查
  3. 03构建经过验证的数据库功能
  4. 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