SuperAPI AI Directory

adonisjs

Coding & development

View repository

Updated 10-16 06:24

About this skill

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