安装
npx skills add https://github.com/futuregerald/futuregerald-claude-plugin --skill adonisjs-best-practices
- AdonisJS v6 Best Practices
- Overview
- AdonisJS v6 is a TypeScript-first MVC framework with batteries included. Core principle:
- type safety, dependency injection, and convention over configuration
- .
- 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
- Quick Reference
- 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')
- 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
- Routing
- 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 })
- Controllers
- 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
- )
- }
- }
- Validation
- 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'
- }
- )
- )
- ,
- }
- )
- )
- Middleware
- 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
- (
- )
- )
- Authentication
- 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'
]
}
)
)
Authorization (Bouncer)
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)) { ... }
}
Database (Lucid ORM)
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
}
)
}
)
Error Handling
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
)
}
}
Testing
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
)
}
)
}
)
Common Mistakes
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
← 返回排行榜