September 27, 2026

AdonisJS.

Mengenal AdonisJS 7: framework Node.js ala Laravel — Ace CLI, routing, middleware, IoC container, VineJS, Lucid ORM dengan schema class ter-generate, Japa testing, dan contoh CRUD yang sudah dites.

AdonisJS adalah framework Node.js full-featured berbasis TypeScript yang sering disebut “Laravel-nya Node.js”. Buat yang terbiasa dengan Laravel, konsepnya akan terasa sangat familiar: CLI ace (mirip artisan), ORM Active Record (Lucid ≈ Eloquent), validator (VineJS), service provider, IoC container, dan test runner (Japa). Aku sedang mengeksplorasi AdonisJS, jadi catatan ini sekaligus rangkuman eksperimen.

Versi yang dibahas: AdonisJS 7 (@adonisjs/core 7.5.x, @adonisjs/lucid 22.x, @vinejs/vine 4.x, @japa/runner 5.x — per September 2026). AdonisJS 7 butuh Node.js ≥ 24 dan npm ≥ 11.


1. Filosofi

  • Batteries included — auth, session, validasi, ORM, mail, rate limiter, CORS, shield (CSRF/security header), hashing, dan test runner tersedia sebagai paket resmi yang saling terintegrasi.
  • Konvensi di atas konfigurasi — struktur folder, penamaan file (posts_controller.ts), dan alias import (#models/post) sudah ditentukan.
  • Type-safety end-to-end — v7 banyak menghasilkan kode otomatis (folder .adonisjs/): barrel controller, tipe route, schema class model dari database, sampai kontrak tipe untuk frontend.
  • Dibangun sendiri, bukan wrapper — HTTP server, router, dan container adalah paket AdonisJS sendiri (@adonisjs/http-server, dst.), bukan lapisan di atas Express.

2. Membuat Project & Struktur Folder

npm create adonisjs@latest adonis-demo -- --kit=api
cd adonis-demo
npm run dev   # http://localhost:3333

Starter kit resmi: hypermedia (Edge + Alpine.js), react dan vue (Inertia), api, dan api-monorepo. Kit API memakai SQLite + Lucid secara default dan sudah menyertakan auth berbasis access token.

Struktur hasil kit api (disederhanakan):

adonis-demo/
├── .adonisjs/                # KODE TER-GENERATE — jangan diedit
│   ├── client/               # tipe route/registry untuk client & test
│   └── server/controllers.ts # barrel → import { controllers } from '#generated/controllers'
├── app/
│   ├── controllers/          # posts_controller.ts
│   ├── exceptions/handler.ts # global exception handler
│   ├── middleware/           # auth_middleware.ts, dll.
│   ├── models/               # post.ts (extends PostSchema)
│   ├── transformers/         # bentuk output JSON (baru di v7)
│   └── validators/           # post.ts (VineJS)
├── bin/
│   ├── server.ts             # entry HTTP server
│   ├── console.ts            # entry Ace CLI
│   └── test.ts               # entry test runner
├── config/                   # app, auth, database, cors, hash, session, ...
├── database/
│   ├── migrations/
│   ├── schema.ts             # schema class ter-generate dari DB
│   └── schema_rules.ts
├── providers/                # service provider
├── start/
│   ├── env.ts                # validasi environment variable
│   ├── kernel.ts             # registrasi middleware
│   └── routes.ts             # definisi route
├── tests/
│   ├── bootstrap.ts
│   └── functional/
├── ace.js
├── adonisrc.ts               # manifest project: providers, preloads, suites test
└── package.json

Generator Ace:

node ace make:model Post -m          # model + migration
node ace make:controller posts --resource
node ace make:validator post
node ace make:middleware log_request
node ace make:test posts --suite=functional
node ace list                        # lihat semua perintah

3. Routing

Route didefinisikan di start/routes.ts. Controller dirujuk lewat barrel #generated/controllers (di-generate otomatis ke .adonisjs/server/controllers.ts) — controller tetap di-lazy load.

import router from '@adonisjs/core/services/router'
import { middleware } from '#start/kernel'
import { controllers } from '#generated/controllers'

router.get('/', () => ({ hello: 'world' }))

// route dengan controller + nama route
router.get('/posts/:id', [controllers.Posts, 'show']).as('posts.show')

// resource: index, show, store, update, destroy (tanpa create/edit)
router.resource('posts', controllers.Posts).apiOnly()

// group dengan prefix + middleware
router
  .group(() => {
    router.get('profile', [controllers.Profile, 'show'])
  })
  .prefix('/api/v1/account')
  .use(middleware.auth())

Named route dimanfaatkan untuk URL builder yang ter-typing, juga di test (client.visit('posts.show', ...)).


4. Middleware

Middleware adalah class dengan method handle(ctx, next):

// app/middleware/log_request_middleware.ts
import type { HttpContext } from '@adonisjs/core/http'
import type { NextFn } from '@adonisjs/core/types/http'

export default class LogRequestMiddleware {
  async handle(ctx: HttpContext, next: NextFn) {
    const start = performance.now()
    await next() // jalankan handler
    ctx.logger.info(`${ctx.request.method()} ${ctx.request.url()} ${(performance.now() - start).toFixed(1)}ms`)
  }
}

Registrasi di start/kernel.ts ada tiga level:

LevelAPIKapan jalan
Serverserver.use([...])Setiap request, bahkan jika route tidak ada
Routerrouter.use([...])Setiap request yang cocok dengan route
Namedrouter.named({ auth: ... })Hanya di route yang memanggil .use(middleware.auth())
// start/kernel.ts (potongan dari kit api)
router.use([
  () => import('@adonisjs/core/bodyparser_middleware'),
  () => import('@adonisjs/auth/initialize_auth_middleware'),
])

export const middleware = router.named({
  auth: () => import('#middleware/auth_middleware'),
})

5. IoC Container & Dependency Injection

AdonisJS punya IoC container bawaan. Class bisa diinjeksi lewat decorator @inject() — di constructor maupun di method.

import { inject } from '@adonisjs/core'
import type { HttpContext } from '@adonisjs/core/http'
import PostService from '#services/post_service'

@inject()
export default class PostsController {
  constructor(protected postService: PostService) {}

  async index(ctx: HttpContext) {
    return this.postService.latest()
  }
}

Service provider (providers/*.ts) dipakai untuk mendaftarkan binding, singleton, atau menjalankan kode saat boot — mirip AppServiceProvider di Laravel. Tidak ada konsep “module” seperti NestJS; organisasi kode mengikuti folder app/.


6. Validasi (VineJS)

VineJS adalah validator buatan tim AdonisJS. Di v7, validator dibuat dengan vine.create():

// app/validators/post.ts
import vine from '@vinejs/vine'

export const createPostValidator = vine.create({
  title: vine.string().trim().minLength(3),
  body: vine.string().trim(),
})

Di controller cukup request.validateUsing(). Tidak perlu try/catch — global exception handler mengubah error validasi menjadi 422 berisi JSON untuk API, atau redirect + flash message untuk aplikasi server-rendered.

Aturan yang butuh database pun tersedia, misalnya vine.string().email().unique({ table: 'users', column: 'email' }).


7. Data Layer: Lucid ORM

Lucid adalah ORM Active Record di atas Knex, mendukung PostgreSQL, MySQL/MariaDB, SQLite, dan MSSQL.

Perubahan besar di v7: migrations-first. Kamu menulis migration, menjalankan node ace migration:run, lalu Lucid menggenerate schema class di database/schema.ts. Model cukup meng-extend class itu.

// database/migrations/xxxx_create_posts_table.ts
import { BaseSchema } from '@adonisjs/lucid/schema'

export default class extends BaseSchema {
  protected tableName = 'posts'

  async up() {
    this.schema.createTable(this.tableName, (table) => {
      table.increments('id')
      table.string('title').notNullable()
      table.text('body').notNullable()
      table.timestamp('created_at')
      table.timestamp('updated_at')
    })
  }

  async down() {
    this.schema.dropTable(this.tableName)
  }
}

Setelah migration:run, database/schema.ts berisi (jangan diedit manual):

export class PostSchema extends BaseModel {
  @column() declare body: string
  @column.dateTime({ autoCreate: true }) declare createdAt: DateTime | null
  @column({ isPrimary: true }) declare id: number
  @column() declare title: string
  @column.dateTime({ autoCreate: true, autoUpdate: true }) declare updatedAt: DateTime | null
}
// app/models/post.ts
import { PostSchema } from '#database/schema'

export default class Post extends PostSchema {
  // relasi, hooks, computed, scope ditulis di sini
}

Query sehari-hari:

await Post.all()
await Post.findOrFail(1)              // 404 otomatis via exception handler
await Post.query().where('title', 'like', '%adonis%').orderBy('id', 'desc').paginate(1, 10)
await Post.create({ title: 'Halo', body: '...' })
Perintah AceFungsi
node ace make:migration postsBuat file migration
node ace migration:runJalankan migration + generate schema class
node ace migration:rollbackRollback batch terakhir
node ace make:seeder User / db:seedSeeder
node ace make:factory PostFactory untuk test

Untuk output JSON, v7 memperkenalkan transformers (app/transformers/) — memilih field mana yang dikirim, mirip API Resource di Laravel.


8. Testing (Japa)

AdonisJS memakai Japa dengan plugin API client, assertion database, dan helper auth sudah terpasang di tests/bootstrap.ts. Suite unit dan functional didefinisikan di adonisrc.ts.

// tests/functional/posts.spec.ts
import { test } from '@japa/runner'
import testUtils from '@adonisjs/core/services/test_utils'

test.group('Posts', (group) => {
  // setiap test dibungkus transaksi lalu di-rollback
  group.each.setup(() => testUtils.db().withGlobalTransaction())

  test('membuat post baru', async ({ client }) => {
    const response = await client.post('/posts').json({ title: 'Halo Adonis', body: 'Isi post' })
    response.assertStatus(201)
    response.assertBodyContains({ title: 'Halo Adonis' })
  })

  test('menolak title yang terlalu pendek', async ({ client }) => {
    const response = await client.post('/posts').json({ title: 'Hi', body: 'x' })
    response.assertStatus(422)
  })
})
node ace test                 # semua suite
node ace test functional      # suite tertentu
node ace test --watch

9. Performa

Karena HTTP server AdonisJS ditulis sendiri (bukan di atas Express), overhead-nya cukup rendah. Di benchmark fastify/benchmarks (run 2 September 2026, Node v24.20.0, 4 vCPU, autocannon -c 100 -d 40 -p 10, endpoint “hello world” JSON), baris adonisjs mencatat sekitar 89.837 req/s — di bawah Fastify 5.12.1 (≈97.595) tapi jauh di atas Express 5.2.1 (≈59.651).

Catatan penting soal angka itu:

  • Versi yang tercantum 9.3.0 adalah versi @adonisjs/http-server, bukan framework penuh v7. Benchmark-nya hanya menyalakan HTTP server + router AdonisJS, tanpa session, shield, auth, bodyparser, dan middleware lain yang ada di aplikasi nyata.
  • Aplikasi dari starter kit menjalankan beberapa middleware per request (lihat start/kernel.ts), jadi throughput riilnya lebih rendah. Hapus middleware yang tidak dipakai jika performa jadi perhatian.
  • Seperti biasa, query database biasanya jauh lebih dominan dibanding overhead framework.

10. Ekosistem & Kematangan

AspekPenilaian
UmurSejak 2015; v5 beralih ke TypeScript, v6 ke ESM, v7 rilis Februari 2026
Paket resmiauth, session, shield, cors, limiter, mail, drive, bouncer (otorisasi), ally (OAuth), lock, cache, transmit (SSE), inertia, vite
KomunitasLebih kecil dari Express/Nest/Next, tapi dokumentasi dan video (Adocasts) sangat baik
Kontrol kualitasTim inti kecil, API konsisten antar paket
RisikoLibrary pihak ketiga lebih sedikit; kadang harus bikin integrasi sendiri
Dokumentasidocs.adonisjs.com

11. Contoh CRUD Minimal (Sudah Dites)

Contoh ini aku jalankan di kit api (Node 25, AdonisJS 7.5.2) dan kedua test di atas lolos.

npm create adonisjs@latest adonis-demo -- --kit=api
cd adonis-demo
node ace make:model Post -m
node ace make:controller posts --resource
node ace make:validator post
node ace make:test posts --suite=functional
  1. Isi migration dan validator seperti di bagian 6 & 7, lalu node ace migration:run.
  2. Controller:
// app/controllers/posts_controller.ts
import type { HttpContext } from '@adonisjs/core/http'
import Post from '#models/post'
import { createPostValidator } from '#validators/post'

export default class PostsController {
  async index() {
    return Post.query().orderBy('id', 'desc')
  }

  async show({ params }: HttpContext) {
    return Post.findOrFail(params.id)
  }

  async store({ request, response }: HttpContext) {
    const payload = await request.validateUsing(createPostValidator)
    const post = await Post.create(payload)
    return response.created(post)
  }
}
  1. Tambahkan di start/routes.ts:
router.resource('posts', controllers.Posts).only(['index', 'show', 'store'])
  1. Jalankan:
npm run dev
curl -X POST localhost:3333/posts -H 'Content-Type: application/json' \
  -d '{"title":"Halo Adonis","body":"Isi post"}'
curl localhost:3333/posts

node ace test

12. Kapan Memilih AdonisJS?

  • Datang dari Laravel dan ingin pengalaman serupa di TypeScript.
  • Aplikasi CRUD/SaaS/monolith yang butuh auth, session, ORM, mail, dan validasi siap pakai.
  • Full-stack dengan Inertia (React/Vue) tanpa harus memisah repo frontend-backend.

Kalau lebih butuh API super ringan, lihat Fastify ; kalau butuh arsitektur modular skala enterprise, lihat NestJS . Ringkasannya ada di Perbandingan Framework Node.js .


Referensi

Hey! I’m Fanny, the software engineer tending to this digital garden. You can read more about me, or subscribe by email.

Comments