Use when building AdonisJS v6 applications, implementing features in AdonisJS, or reviewing AdonisJS code. Covers routing, controllers, validation, authentication, database patterns, testing, and error handling.
Provides AdonisJS v6 best practices for routing, controllers, validation, database patterns, and testing.
npx claudepluginhub futuregerald/futuregerald-claude-pluginThis skill inherits all available tools. When active, it can use any tool Claude has access to.
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-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 |
Expert guidance for Next.js Cache Components and Partial Prerendering (PPR). **PROACTIVE ACTIVATION**: Use this skill automatically when working in Next.js projects that have `cacheComponents: true` in their next.config.ts/next.config.js. When this config is detected, proactively apply Cache Components patterns and best practices to all React Server Component implementations. **DETECTION**: At the start of a session in a Next.js project, check for `cacheComponents: true` in next.config. If enabled, this skill's patterns should guide all component authoring, data fetching, and caching decisions. **USE CASES**: Implementing 'use cache' directive, configuring cache lifetimes with cacheLife(), tagging cached data with cacheTag(), invalidating caches with updateTag()/revalidateTag(), optimizing static vs dynamic content boundaries, debugging cache issues, and reviewing Cache Component implementations.
Applies Anthropic's official brand colors and typography to any sort of artifact that may benefit from having Anthropic's look-and-feel. Use it when brand colors or style guidelines, visual formatting, or company design standards apply.
Creating algorithmic art using p5.js with seeded randomness and interactive parameter exploration. Use this when users request creating art using code, generative art, algorithmic art, flow fields, or particle systems. Create original algorithmic art rather than copying existing artists' work to avoid copyright violations.