AdonisJS Backend Skill
Boundary With 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.
Core Conventions
Routing (start/routes.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/)
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/)
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/)
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
// 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
| Situation |
File |
| Implementing any new feature |
workflows/build-feature.md |
| Debugging errors, unexpected behavior |
workflows/debug.md |
| Creating Service Providers, bindings, IoC |
workflows/providers.md |
| Customizing errors, domain exceptions |
workflows/exceptions.md |
| Decoupling side effects with events |
workflows/events.md |
| Reviewing a PR or code snippet |
workflows/code-review.md |
References
| Topic |
File |
| Architecture, folder structure, three rendering modes |
references/architecture.md |
| Auth, guards, Bouncer (.authorize vs .allows) |
references/auth.md |
| Cache, invalidation, TTL, tags |
references/cache.md |
| Controllers, resource vs action, route ordering |
references/controllers.md |
| Events, listeners, emitter |
references/events.md |
| Exceptions handler, custom domain errors |
references/exceptions.md |
| HTTP — request, response, session, URL builder |
references/http.md |
| Mail — send, mail classes, templates, testing |
references/mail.md |
| Middleware — named, params, global |
references/middleware.md |
| Performance — cache, queues, deferred expensive work |
references/performance.md |
| Queue, jobs, retry, workers |
references/queue.md |
| Security — hashing, encryption, CORS, CSRF |
references/security.md |
| Transformers — BaseTransformer, pick, whenLoaded, variants |
references/transformers.md |
| VineJS validations — vine.create(), all field types |
references/validations.md |
| Ace commands — create, args, flags, prompts |
references/ace-commands.md |
Ace CLI Quick Reference
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
Anti-Patterns
| Wrong |
Correct |
vine.compile() |
vine.create() |
| Inline validation in controller |
Separate app/validators/ file |
| Business logic in controller |
Move to app/services/ |
| Side effects (email) in controller |
Events + Listeners |
import from '@adonisjs/core' directly |
Sub-path: '@adonisjs/core/http' etc. |
| 8+ methods on one controller |
Split into focused Action controllers |
| Email verification behind auth middleware |
Verification 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 |
1---2name: adonisjs3description: Use this skill whenever the user is working with AdonisJS v7 backend framework code: controllers, routes, middleware, services, VineJS validators, Transformers, Bouncer policies, events, listeners, mail, cache, queue, exceptions, Ace commands, request/response/session handling, or backend architecture and review. Trigger for "create a controller", "add validation", "create a service", "add a policy", "wire routes", "handle an exception", or AdonisJS backend review/debugging. For Lucid ORM, migrations, schema generation, models, relationships, query builders, transactions, factories, or seeders, use the lucid skill alongside or instead of this one. For Japa tests, use the japa skill. For Inertia frontend patterns, use inertia-react or inertia-vue alongside this one.4---56# AdonisJS Backend Skill78## Boundary With Lucid910This skill covers the AdonisJS framework layer. Load `lucid` for database and ORM work:1112- Migrations and schema generation13- `database/schema.ts`14- Model files and model hooks/scopes/serialization15- Relationships and preloads16- Query builders and transactions17- Factories and seeders1819AdonisJS controllers, transformers, policies, and services may consume Lucid models, but the rules for defining/querying those models live in `lucid`.2021---2223## Core Conventions2425### Routing (start/routes.ts)2627```ts28import router from '@adonisjs/core/services/router'29import { middleware } from '#start/kernel'30import { controllers } from '#generated/controllers'3132// CRITICAL: fixed routes BEFORE dynamic params33router.get('/posts/create', [controllers.Posts, 'create']).use(middleware.auth())34router.post('/posts', [controllers.Posts, 'store']).use(middleware.auth())35router.get('/posts/:id', [controllers.Posts, 'show'])36router.get('/posts/:id/edit', [controllers.Posts, 'edit']).use(middleware.auth())37router.put('/posts/:id', [controllers.Posts, 'update']).use(middleware.auth())3839// Guest-only40router.group(() => {41 router.get('/login', [controllers.Session, 'create'])42 router.post('/login', [controllers.Session, 'store'])43}).use(middleware.guest())44```4546### Controllers (app/controllers/)4748```ts49import type { HttpContext } from '@adonisjs/core/http'50import PostTransformer from '#transformers/post_transformer'51import PostPolicy from '#policies/post_policy'52import { createPostValidator } from '#validators/post'53import postsService from '#services/posts_service'5455export default class PostsController {56 async index({ inertia }: HttpContext) { // swap inertia for view/serialize per architecture57 const posts = await postsService.listForIndex()58 return inertia.render('posts/index', { posts: PostTransformer.transform(posts) })59 }6061 async store({ request, auth, response }: HttpContext) {62 const payload = await request.validateUsing(createPostValidator)63 await postsService.create(auth.user!, payload)64 return response.redirect().toRoute('posts.index')65 }6667 async update({ bouncer, params, request, response, session }: HttpContext) {68 const post = await postsService.findForUpdate(params.id)69 await bouncer.with(PostPolicy).authorize('edit', post) // throws 403 if denied70 const data = await request.validateUsing(updatePostValidator)71 await postsService.update(post, data)72 session.flash('success', 'Post updated successfully')73 return response.redirect().toRoute('posts.show', { id: post.id })74 }75}76```7778### Transformers (app/transformers/)7980```ts81import { BaseTransformer } from '@adonisjs/core/transformers'82import { inject } from '@adonisjs/core'83import { HttpContext } from '@adonisjs/core/http'84import type Post from '#models/post'85import PostPolicy from '#policies/post_policy'8687export default class PostTransformer extends BaseTransformer<Post> {88 toObject() {89 return {90 ...this.pick(this.resource, ['id', 'title', 'url', 'summary', 'createdAt']),91 author: UserTransformer.transform(this.resource.author),92 // whenLoaded() — omits field if relation was not preloaded93 comments: CommentTransformer.transform(this.whenLoaded(this.resource.comments)),94 }95 }9697 // Variant — extends toObject() with permission flags98 @inject()99 async forDetailedView({ bouncer }: HttpContext) {100 return {101 ...this.toObject(),102 can: {103 edit: await bouncer.with(PostPolicy).allows('edit', this.resource),104 delete: await bouncer.with(PostPolicy).allows('delete', this.resource),105 },106 }107 }108}109110// Single: PostTransformer.transform(post)111// Array: PostTransformer.transform(posts)112// Variant: PostTransformer.transform(post).useVariant('forDetailedView')113// Paginated: PostTransformer.paginate(posts.all(), posts.getMeta())114```115116### Validation (app/validators/)117118```ts119import vine from '@vinejs/vine'120121// vine.create() — not vine.compile()122export const createPostValidator = vine.create({123 title: vine.string().trim().minLength(3).maxLength(255),124 url: vine.string().url(),125 summary: vine.string().trim().minLength(80).maxLength(500),126})127128// Clone schema to reuse rules for update129export const updatePostValidator = vine.create(130 createPostValidator.schema.clone()131)132```133134---135136## Critical Import Rules137138```ts139// WRONG — never import from root package140import { HttpContext } from '@adonisjs/core'141142// CORRECT sub-path imports143import type { HttpContext } from '@adonisjs/core/http'144import { inject } from '@adonisjs/core'145import { BaseTransformer } from '@adonisjs/core/transformers'146import { BasePolicy } from '@adonisjs/bouncer'147import router from '@adonisjs/core/services/router'148import { urlFor, signedUrlFor } from '@adonisjs/core/services/url_builder'149import vine from '@vinejs/vine'150import Post from '#models/post'151import { controllers } from '#generated/controllers'152```153154---155156## Workflows157158| Situation | File |159|---|---|160| Implementing any new feature | workflows/build-feature.md |161| Debugging errors, unexpected behavior | workflows/debug.md |162| Creating Service Providers, bindings, IoC | workflows/providers.md |163| Customizing errors, domain exceptions | workflows/exceptions.md |164| Decoupling side effects with events | workflows/events.md |165| Reviewing a PR or code snippet | workflows/code-review.md |166167---168169## References170171| Topic | File |172|---|---|173| Architecture, folder structure, three rendering modes | references/architecture.md |174| Auth, guards, Bouncer (.authorize vs .allows) | references/auth.md |175| Cache, invalidation, TTL, tags | references/cache.md |176| Controllers, resource vs action, route ordering | references/controllers.md |177| Events, listeners, emitter | references/events.md |178| Exceptions handler, custom domain errors | references/exceptions.md |179| HTTP — request, response, session, URL builder | references/http.md |180| Mail — send, mail classes, templates, testing | references/mail.md |181| Middleware — named, params, global | references/middleware.md |182| Performance — cache, queues, deferred expensive work | references/performance.md |183| Queue, jobs, retry, workers | references/queue.md |184| Security — hashing, encryption, CORS, CSRF | references/security.md |185| Transformers — BaseTransformer, pick, whenLoaded, variants | references/transformers.md |186| VineJS validations — vine.create(), all field types | references/validations.md |187| Ace commands — create, args, flags, prompts | references/ace-commands.md |188189---190191## Ace CLI Quick Reference192193```bash194node ace make:controller Post --resource195node ace make:validator post196node ace make:transformer post197node ace make:policy post198node ace make:service PostService199node ace make:event OrderPlaced200node ace make:listener SendEmail --event=OrderPlaced201node ace make:job ProcessImage202node ace make:middleware AuthMiddleware203node ace make:exception DomainException204node ace make:command SendReminders205node ace make:mail OrderConfirmation206node ace list:routes207node ace repl208node ace generate:key209# Types in @generated/data are auto-generated by the dev server — no manual command210```211212---213214## Anti-Patterns215216| Wrong | Correct |217|---|---|218| `vine.compile()` | `vine.create()` |219| Inline validation in controller | Separate `app/validators/` file |220| Business logic in controller | Move to `app/services/` |221| Side effects (email) in controller | Events + Listeners |222| `import from '@adonisjs/core'` directly | Sub-path: `'@adonisjs/core/http'` etc. |223| 8+ methods on one controller | Split into focused Action controllers |224| Email verification behind auth middleware | Verification routes must be public |225| Dynamic route (`:id`) before fixed route (`/create`) | Fixed routes first |226| `bouncer.authorize()` in transformers | `bouncer.allows()` in transformers |227| `router.makeUrl()` | `urlFor()` from `@adonisjs/core/services/url_builder` |228| `{{ route('...') }}` in Edge | `{{ urlFor('...') }}` in Edge |229| Missing `prefixUrl` in `signedUrlFor` for emails | Always pass `prefixUrl` for external links |