adonisjs-best-practices

安装量: 82
排名: #9623

安装

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
返回排行榜