adonisjs

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

AdonisJS Backend Skill

AdonisJS后端技能

Boundary With Lucid

与Lucid的边界划分

This skill covers the AdonisJS framework layer. Load
lucid
for database and ORM work:
  • Migrations and schema generation
  • database/schema.ts
  • Model files and model hooks/scopes/serialization
  • Relationships and preloads
  • Query builders and transactions
  • Factories and seeders
AdonisJS controllers, transformers, policies, and services may consume Lucid models, but the rules for defining/querying those models live in
lucid
.

此技能覆盖AdonisJS框架层。涉及数据库和ORM相关工作时,请加载
lucid
技能:
  • 迁移与Schema生成
  • database/schema.ts
  • 模型文件及模型钩子/作用域/序列化
  • 关联与预加载
  • 查询构建器与事务
  • 工厂与种子器
AdonisJS的控制器、转换器、策略和服务可能会调用Lucid模型,但模型的定义/查询规则属于
lucid
技能的范畴。

Core Conventions

核心约定

Routing (start/routes.ts)

路由(start/routes.ts)

ts
import router from '@adonisjs/core/services/router'
import { middleware } from '#start/kernel'
import { controllers } from '#generated/controllers'

// CRITICAL: fixed routes BEFORE dynamic params
router.get('/posts/create', [controllers.Posts, 'create']).use(middleware.auth())
router.post('/posts', [controllers.Posts, 'store']).use(middleware.auth())
router.get('/posts/:id', [controllers.Posts, 'show'])
router.get('/posts/:id/edit', [controllers.Posts, 'edit']).use(middleware.auth())
router.put('/posts/:id', [controllers.Posts, 'update']).use(middleware.auth())

// Guest-only
router.group(() => {
  router.get('/login', [controllers.Session, 'create'])
  router.post('/login', [controllers.Session, 'store'])
}).use(middleware.guest())
ts
import router from '@adonisjs/core/services/router'
import { middleware } from '#start/kernel'
import { controllers } from '#generated/controllers'

// CRITICAL: fixed routes BEFORE dynamic params
router.get('/posts/create', [controllers.Posts, 'create']).use(middleware.auth())
router.post('/posts', [controllers.Posts, 'store']).use(middleware.auth())
router.get('/posts/:id', [controllers.Posts, 'show'])
router.get('/posts/:id/edit', [controllers.Posts, 'edit']).use(middleware.auth())
router.put('/posts/:id', [controllers.Posts, 'update']).use(middleware.auth())

// Guest-only
router.group(() => {
  router.get('/login', [controllers.Session, 'create'])
  router.post('/login', [controllers.Session, 'store'])
}).use(middleware.guest())

Controllers (app/controllers/)

控制器(app/controllers/)

ts
import type { HttpContext } from '@adonisjs/core/http'
import PostTransformer from '#transformers/post_transformer'
import PostPolicy from '#policies/post_policy'
import { createPostValidator } from '#validators/post'
import postsService from '#services/posts_service'

export default class PostsController {
  async index({ inertia }: HttpContext) {   // swap inertia for view/serialize per architecture
    const posts = await postsService.listForIndex()
    return inertia.render('posts/index', { posts: PostTransformer.transform(posts) })
  }

  async store({ request, auth, response }: HttpContext) {
    const payload = await request.validateUsing(createPostValidator)
    await postsService.create(auth.user!, payload)
    return response.redirect().toRoute('posts.index')
  }

  async update({ bouncer, params, request, response, session }: HttpContext) {
    const post = await postsService.findForUpdate(params.id)
    await bouncer.with(PostPolicy).authorize('edit', post)  // throws 403 if denied
    const data = await request.validateUsing(updatePostValidator)
    await postsService.update(post, data)
    session.flash('success', 'Post updated successfully')
    return response.redirect().toRoute('posts.show', { id: post.id })
  }
}
ts
import type { HttpContext } from '@adonisjs/core/http'
import PostTransformer from '#transformers/post_transformer'
import PostPolicy from '#policies/post_policy'
import { createPostValidator } from '#validators/post'
import postsService from '#services/posts_service'

export default class PostsController {
  async index({ inertia }: HttpContext) {   // swap inertia for view/serialize per architecture
    const posts = await postsService.listForIndex()
    return inertia.render('posts/index', { posts: PostTransformer.transform(posts) })
  }

  async store({ request, auth, response }: HttpContext) {
    const payload = await request.validateUsing(createPostValidator)
    await postsService.create(auth.user!, payload)
    return response.redirect().toRoute('posts.index')
  }

  async update({ bouncer, params, request, response, session }: HttpContext) {
    const post = await postsService.findForUpdate(params.id)
    await bouncer.with(PostPolicy).authorize('edit', post)  // throws 403 if denied
    const data = await request.validateUsing(updatePostValidator)
    await postsService.update(post, data)
    session.flash('success', 'Post updated successfully')
    return response.redirect().toRoute('posts.show', { id: post.id })
  }
}

Transformers (app/transformers/)

转换器(app/transformers/)

ts
import { BaseTransformer } from '@adonisjs/core/transformers'
import { inject } from '@adonisjs/core'
import { HttpContext } from '@adonisjs/core/http'
import type Post from '#models/post'
import PostPolicy from '#policies/post_policy'

export default class PostTransformer extends BaseTransformer<Post> {
  toObject() {
    return {
      ...this.pick(this.resource, ['id', 'title', 'url', 'summary', 'createdAt']),
      author: UserTransformer.transform(this.resource.author),
      // whenLoaded() — omits field if relation was not preloaded
      comments: CommentTransformer.transform(this.whenLoaded(this.resource.comments)),
    }
  }

  // Variant — extends toObject() with permission flags
  @inject()
  async forDetailedView({ bouncer }: HttpContext) {
    return {
      ...this.toObject(),
      can: {
        edit: await bouncer.with(PostPolicy).allows('edit', this.resource),
        delete: await bouncer.with(PostPolicy).allows('delete', this.resource),
      },
    }
  }
}

// Single:    PostTransformer.transform(post)
// Array:     PostTransformer.transform(posts)
// Variant:   PostTransformer.transform(post).useVariant('forDetailedView')
// Paginated: PostTransformer.paginate(posts.all(), posts.getMeta())
ts
import { BaseTransformer } from '@adonisjs/core/transformers'
import { inject } from '@adonisjs/core'
import { HttpContext } from '@adonisjs/core/http'
import type Post from '#models/post'
import PostPolicy from '#policies/post_policy'

export default class PostTransformer extends BaseTransformer<Post> {
  toObject() {
    return {
      ...this.pick(this.resource, ['id', 'title', 'url', 'summary', 'createdAt']),
      author: UserTransformer.transform(this.resource.author),
      // whenLoaded() — omits field if relation was not preloaded
      comments: CommentTransformer.transform(this.whenLoaded(this.resource.comments)),
    }
  }

  // Variant — extends toObject() with permission flags
  @inject()
  async forDetailedView({ bouncer }: HttpContext) {
    return {
      ...this.toObject(),
      can: {
        edit: await bouncer.with(PostPolicy).allows('edit', this.resource),
        delete: await bouncer.with(PostPolicy).allows('delete', this.resource),
      },
    }
  }
}

// Single:    PostTransformer.transform(post)
// Array:     PostTransformer.transform(posts)
// Variant:   PostTransformer.transform(post).useVariant('forDetailedView')
// Paginated: PostTransformer.paginate(posts.all(), posts.getMeta())

Validation (app/validators/)

验证(app/validators/)

ts
import vine from '@vinejs/vine'

// vine.create() — not vine.compile()
export const createPostValidator = vine.create({
  title: vine.string().trim().minLength(3).maxLength(255),
  url: vine.string().url(),
  summary: vine.string().trim().minLength(80).maxLength(500),
})

// Clone schema to reuse rules for update
export const updatePostValidator = vine.create(
  createPostValidator.schema.clone()
)

ts
import vine from '@vinejs/vine'

// vine.create() — not vine.compile()
export const createPostValidator = vine.create({
  title: vine.string().trim().minLength(3).maxLength(255),
  url: vine.string().url(),
  summary: vine.string().trim().minLength(80).maxLength(500),
})

// Clone schema to reuse rules for update
export const updatePostValidator = vine.create(
  createPostValidator.schema.clone()
)

Critical Import Rules

关键导入规则

ts
// WRONG — never import from root package
import { HttpContext } from '@adonisjs/core'

// CORRECT sub-path imports
import type { HttpContext } from '@adonisjs/core/http'
import { inject } from '@adonisjs/core'
import { BaseTransformer } from '@adonisjs/core/transformers'
import { BasePolicy } from '@adonisjs/bouncer'
import router from '@adonisjs/core/services/router'
import { urlFor, signedUrlFor } from '@adonisjs/core/services/url_builder'
import vine from '@vinejs/vine'
import Post from '#models/post'
import { controllers } from '#generated/controllers'

ts
// WRONG — never import from root package
import { HttpContext } from '@adonisjs/core'

// CORRECT sub-path imports
import type { HttpContext } from '@adonisjs/core/http'
import { inject } from '@adonisjs/core'
import { BaseTransformer } from '@adonisjs/core/transformers'
import { BasePolicy } from '@adonisjs/bouncer'
import router from '@adonisjs/core/services/router'
import { urlFor, signedUrlFor } from '@adonisjs/core/services/url_builder'
import vine from '@vinejs/vine'
import Post from '#models/post'
import { controllers } from '#generated/controllers'

Workflows

工作流

SituationFile
Implementing any new featureworkflows/build-feature.md
Debugging errors, unexpected behaviorworkflows/debug.md
Creating Service Providers, bindings, IoCworkflows/providers.md
Customizing errors, domain exceptionsworkflows/exceptions.md
Decoupling side effects with eventsworkflows/events.md
Reviewing a PR or code snippetworkflows/code-review.md

场景文件
实现任何新功能workflows/build-feature.md
调试错误、异常行为workflows/debug.md
创建服务提供者、绑定、IoCworkflows/providers.md
自定义错误、领域异常workflows/exceptions.md
使用事件解耦副作用workflows/events.md
评审PR或代码片段workflows/code-review.md

References

参考资料

TopicFile
Architecture, folder structure, three rendering modesreferences/architecture.md
Auth, guards, Bouncer (.authorize vs .allows)references/auth.md
Cache, invalidation, TTL, tagsreferences/cache.md
Controllers, resource vs action, route orderingreferences/controllers.md
Events, listeners, emitterreferences/events.md
Exceptions handler, custom domain errorsreferences/exceptions.md
HTTP — request, response, session, URL builderreferences/http.md
Mail — send, mail classes, templates, testingreferences/mail.md
Middleware — named, params, globalreferences/middleware.md
Performance — cache, queues, deferred expensive workreferences/performance.md
Queue, jobs, retry, workersreferences/queue.md
Security — hashing, encryption, CORS, CSRFreferences/security.md
Transformers — BaseTransformer, pick, whenLoaded, variantsreferences/transformers.md
VineJS validations — vine.create(), all field typesreferences/validations.md
Ace commands — create, args, flags, promptsreferences/ace-commands.md

主题文件
架构、目录结构、三种渲染模式references/architecture.md
认证、守卫、Bouncer(.authorize vs .allows)references/auth.md
缓存、失效、TTL、标签references/cache.md
控制器、资源vs动作、路由顺序references/controllers.md
事件、监听器、发射器references/events.md
异常处理器、自定义领域错误references/exceptions.md
HTTP — 请求、响应、会话、URL构建器references/http.md
邮件 — 发送、邮件类、模板、测试references/mail.md
中间件 — 命名、参数、全局references/middleware.md
性能 — 缓存、队列、延迟处理耗时任务references/performance.md
队列、任务、重试、工作进程references/queue.md
安全 — 哈希、加密、CORS、CSRFreferences/security.md
转换器 — BaseTransformer、pick、whenLoaded、变体references/transformers.md
VineJS验证 — vine.create()、所有字段类型references/validations.md
Ace命令 — 创建、参数、标志、提示references/ace-commands.md

Ace CLI Quick Reference

Ace CLI快速参考

bash
node ace make:controller Post --resource
node ace make:validator post
node ace make:transformer post
node ace make:policy post
node ace make:service PostService
node ace make:event OrderPlaced
node ace make:listener SendEmail --event=OrderPlaced
node ace make:job ProcessImage
node ace make:middleware AuthMiddleware
node ace make:exception DomainException
node ace make:command SendReminders
node ace make:mail OrderConfirmation
node ace list:routes
node ace repl
node ace generate:key
bash
node ace make:controller Post --resource
node ace make:validator post
node ace make:transformer post
node ace make:policy post
node ace make:service PostService
node ace make:event OrderPlaced
node ace make:listener SendEmail --event=OrderPlaced
node ace make:job ProcessImage
node ace make:middleware AuthMiddleware
node ace make:exception DomainException
node ace make:command SendReminders
node ace make:mail OrderConfirmation
node ace list:routes
node ace repl
node ace generate:key

Types in @generated/data are auto-generated by the dev server — no manual command

Types in @generated/data are auto-generated by the dev server — no manual command


---

---

Anti-Patterns

反模式

WrongCorrect
vine.compile()
vine.create()
Inline validation in controllerSeparate
app/validators/
file
Business logic in controllerMove to
app/services/
Side effects (email) in controllerEvents + Listeners
import from '@adonisjs/core'
directly
Sub-path:
'@adonisjs/core/http'
etc.
8+ methods on one controllerSplit into focused Action controllers
Email verification behind auth middlewareVerification routes must be public
Dynamic route (
:id
) before fixed route (
/create
)
Fixed routes first
bouncer.authorize()
in transformers
bouncer.allows()
in transformers
router.makeUrl()
urlFor()
from
@adonisjs/core/services/url_builder
{{ route('...') }}
in Edge
{{ urlFor('...') }}
in Edge
Missing
prefixUrl
in
signedUrlFor
for emails
Always pass
prefixUrl
for external links
错误做法正确做法
vine.compile()
vine.create()
在控制器中内联验证逻辑单独创建
app/validators/
文件
业务逻辑写在控制器中迁移到
app/services/
在控制器中处理副作用(如发送邮件)使用事件 + 监听器
直接从
'@adonisjs/core'
导入
使用子路径:
'@adonisjs/core/http'
单个控制器包含8个以上方法拆分为专注单一功能的Action控制器
邮件验证放在认证中间件之后验证路由必须公开
动态路由(
:id
)放在固定路由(
/create
)之前
固定路由优先
在转换器中使用
bouncer.authorize()
在转换器中使用
bouncer.allows()
使用
router.makeUrl()
使用
@adonisjs/core/services/url_builder
中的
urlFor()
在Edge模板中使用
{{ route('...') }}
在Edge模板中使用
{{ urlFor('...') }}
在邮件的
signedUrlFor
中缺少
prefixUrl
外部链接必须始终传入
prefixUrl