Ultimate AdonisJS Cheatsheet: A Comprehensive Guide to Node.js Framework

Introduction to AdonisJS

AdonisJS is a full-stack Node.js framework focused on developer ergonomics, stability, and confidence. Built on TypeScript, it provides a robust structure for building web applications with features like ORM, authentication, routing, and more out-of-the-box. AdonisJS follows Laravel-inspired conventions, making it accessible for developers familiar with PHP frameworks while leveraging the speed and flexibility of Node.js.

Core Concepts

MVC Architecture

  • Models: Data and business logic
  • Views: Presentation layer (Edge templates)
  • Controllers: Request handling logic

Directory Structure

 
my-app/
  ├── app/          # Application code
  │   ├── Controllers/
  │   ├── Models/
  │   ├── Middleware/
  │   └── Services/
  ├── config/       # Configuration files
  ├── contracts/    # TypeScript interfaces
  ├── database/     # Migrations, seeds, factories
  ├── providers/    # Service providers
  ├── public/       # Static assets
  ├── resources/    # Views, CSS, JS
  ├── start/        # Application bootstrap files
  └── tests/        # Test files

Service Providers

  • Organize and register application components
  • Load only what’s needed to optimize performance
  • Define boot and register lifecycle methods

Installation & Setup

Creating a New Project

 
bash
# Install Adonis CLI
npm i -g @adonisjs/cli

# Create new project
npm init adonis-ts-app@latest my-project

# Project types
# 1. web (API + views)
# 2. api (REST API only)
# 3. slim (minimal setup)

Running the Application

 
bash
# Development with hot-reload
node ace serve --watch

# Production build
node ace build --production
npm run start

Routing

Basic Routes

 
typescript
// start/routes.ts
import Route from '@ioc:Adonis/Core/Route'

// GET request
Route.get('/users', 'UsersController.index')

// POST request
Route.post('/users', 'UsersController.store')

// PUT request
Route.put('/users/:id', 'UsersController.update')

// DELETE request
Route.delete('/users/:id', 'UsersController.destroy')

// Multiple HTTP methods
Route.route('/users', ['GET', 'POST'], 'UsersController.handler')

Route Groups

 
typescript
// Group routes with prefix
Route.group(() => {
  Route.get('/profile', 'UsersController.profile')
  Route.put('/profile', 'UsersController.updateProfile')
}).prefix('/user')

// Apply middleware to a group
Route.group(() => {
  Route.get('/dashboard', 'DashboardController.index')
}).middleware(['auth'])

// Named routes
Route.get('/posts/:id', 'PostsController.show').as('posts.show')

Route Parameters

 
typescript
// Required parameter
Route.get('/posts/:id', 'PostsController.show')

// Optional parameter
Route.get('/posts/:id?', 'PostsController.show')

// Where conditions
Route.get('/posts/:id', 'PostsController.show')
  .where('id', /^[0-9]+$/)

Resource Routes

 
typescript
// Full resource
Route.resource('posts', 'PostsController')
// GET    /posts           - index
// GET    /posts/create    - create
// POST   /posts           - store
// GET    /posts/:id       - show
// GET    /posts/:id/edit  - edit
// PUT    /posts/:id       - update
// DELETE /posts/:id       - destroy

// Limit to specific actions
Route.resource('posts', 'PostsController')
  .only(['index', 'show'])

// API resource (no create/edit)
Route.resource('posts', 'PostsController').apiOnly()

Controllers

Creating Controllers

 
bash
# Create a controller
node ace make:controller User

# Create a resource controller
node ace make:controller User --resource

Basic Controller

 
typescript
// app/Controllers/Http/UsersController.ts
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'

export default class UsersController {
  public async index({ response }: HttpContextContract) {
    return response.json({ message: 'List of users' })
  }

  public async store({ request, response }: HttpContextContract) {
    const data = request.only(['name', 'email'])
    // Save user...
    return response.status(201).json({ message: 'User created' })
  }
}

HTTP Context

 
typescript
// Available properties in HttpContextContract
public async handle({ 
  request,     // HTTP request
  response,    // HTTP response
  params,      // Route parameters
  view,        // View rendering
  session,     // Session management
  auth,        // Authentication
  logger,      // Application logger
  route,       // Current route
  bouncer,     // Authorization
}: HttpContextContract) {
  // Controller logic
}

Database & Models

Configuration

 
typescript
// config/database.ts
export const sqliteConfig = {
  client: 'sqlite',
  connection: {
    filename: Application.tmpPath('db.sqlite3'),
  },
  useNullAsDefault: true,
}

export const postgresConfig = {
  client: 'pg',
  connection: {
    host: Env.get('PG_HOST'),
    port: Env.get('PG_PORT'),
    user: Env.get('PG_USER'),
    password: Env.get('PG_PASSWORD'),
    database: Env.get('PG_DB_NAME'),
  },
}

Migrations

 
bash
# Create migration
node ace make:migration users

# Run migrations
node ace migration:run

# Rollback last migration
node ace migration:rollback

# Reset all migrations
node ace migration:reset

# Refresh migrations (reset + run)
node ace migration:refresh

Migration Example

 
typescript
// database/migrations/users.ts
import BaseSchema from '@ioc:Adonis/Lucid/Schema'

export default class Users extends BaseSchema {
  protected tableName = 'users'

  public async up() {
    this.schema.createTable(this.tableName, (table) => {
      table.increments('id').primary()
      table.string('email', 255).notNullable().unique()
      table.string('password', 180).notNullable()
      table.string('remember_me_token').nullable()
      table.timestamps(true)
    })
  }

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

Models

 
bash
# Create model
node ace make:model User

# Create model with migration
node ace make:model User -m

Model Example

 
typescript
// app/Models/User.ts
import { DateTime } from 'luxon'
import Hash from '@ioc:Adonis/Core/Hash'
import {
  column,
  beforeSave,
  BaseModel,
  hasMany,
  HasMany,
} from '@ioc:Adonis/Lucid/Orm'
import Post from './Post'

export default class User extends BaseModel {
  @column({ isPrimary: true })
  public id: number

  @column()
  public email: string

  @column({ serializeAs: null })
  public password: string

  @column()
  public rememberMeToken: string | null

  @column.dateTime({ autoCreate: true })
  public createdAt: DateTime

  @column.dateTime({ autoCreate: true, autoUpdate: true })
  public updatedAt: DateTime

  @hasMany(() => Post)
  public posts: HasMany<typeof Post>

  @beforeSave()
  public static async hashPassword(user: User) {
    if (user.$dirty.password) {
      user.password = await Hash.make(user.password)
    }
  }
}

Query Builder

 
typescript
// Database query builder
import Database from '@ioc:Adonis/Lucid/Database'

// Select all
const users = await Database.from('users').select('*')

// Where conditions
const user = await Database
  .from('users')
  .where('email', 'user@example.com')
  .first()

// Joins
const posts = await Database
  .from('posts')
  .join('users', 'users.id', 'posts.user_id')
  .select('posts.*', 'users.name')

// Transactions
await Database.transaction(async (trx) => {
  const user = await trx.insertQuery()
    .table('users')
    .insert({ name: 'John' })
  
  await trx.insertQuery()
    .table('profiles')
    .insert({ user_id: user[0], bio: 'Bio' })
})

Using Models

 
typescript
// Find by ID
const user = await User.find(1)

// Find or fail
const user = await User.findOrFail(1)

// First record matching query
const user = await User.query()
  .where('email', 'user@example.com')
  .first()

// Get all
const users = await User.all()

// Filtered query
const users = await User.query()
  .where('role', 'admin')
  .orderBy('created_at', 'desc')

// Create
const user = await User.create({
  email: 'user@example.com',
  password: 'password',
})

// Update
user.email = 'new@example.com'
await user.save()

// Delete
await user.delete()

Relationships

 
typescript
// Model definitions
// User.ts
@hasMany(() => Post)
public posts: HasMany<typeof Post>

// Post.ts
@belongsTo(() => User)
public user: BelongsTo<typeof User>

// Using relationships
// Preload related data
const user = await User.query()
  .where('id', 1)
  .preload('posts')
  .firstOrFail()

// Access related data
const posts = user.posts

// Create related record
const post = await user.related('posts').create({
  title: 'New Post',
  content: 'Content...',
})

Validation

Creating a Validator

 
bash
node ace make:validator CreateUser

Validator Example

 
typescript
// app/Validators/CreateUserValidator.ts
import { schema, rules } from '@ioc:Adonis/Core/Validator'
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'

export default class CreateUserValidator {
  constructor(protected ctx: HttpContextContract) {}

  public schema = schema.create({
    email: schema.string({ trim: true }, [
      rules.email(),
      rules.unique({ table: 'users', column: 'email' }),
    ]),
    password: schema.string({}, [
      rules.confirmed(),
      rules.minLength(8),
    ]),
    age: schema.number([
      rules.range(18, 100),
    ]),
    profile: schema.object().members({
      avatar: schema.string.optional(),
      bio: schema.string.optional(),
    }),
  })

  public messages = {
    'email.required': 'Email is required',
    'email.email': 'Email must be valid',
    'email.unique': 'Email already in use',
    'password.confirmed': 'Passwords do not match',
  }
}

Using Validators in Controllers

 
typescript
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
import CreateUserValidator from 'App/Validators/CreateUserValidator'

export default class UsersController {
  public async store({ request, response }: HttpContextContract) {
    const payload = await request.validate(CreateUserValidator)
    
    // Create user with validated data
    const user = await User.create(payload)
    
    return response.created(user)
  }
}

Authentication

Configuration

 
bash
# Install auth package
npm i @adonisjs/auth

# Configure auth
node ace configure @adonisjs/auth

Auth Middleware

 
typescript
// start/kernel.ts
Server.middleware.registerNamed({
  auth: () => import('App/Middleware/Auth'),
})

// Apply to routes
Route.get('/dashboard', 'DashboardController.index')
  .middleware('auth')

Authentication in Controllers

 
typescript
export default class AuthController {
  // Login
  public async login({ request, auth, response }: HttpContextContract) {
    const { email, password } = request.only(['email', 'password'])
    
    try {
      // For session auth
      await auth.use('web').attempt(email, password)
      return response.redirect('/dashboard')
      
      // For API auth
      const token = await auth.use('api').attempt(email, password)
      return token
    } catch {
      return response.badRequest('Invalid credentials')
    }
  }
  
  // Logout
  public async logout({ auth, response }: HttpContextContract) {
    await auth.logout()
    return response.redirect('/')
  }
  
  // Check auth status
  public async user({ auth }: HttpContextContract) {
    return auth.user
  }
}

Middleware

Creating Middleware

 
bash
node ace make:middleware CheckAdmin

Middleware Example

 
typescript
// app/Middleware/CheckAdmin.ts
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'

export default class CheckAdmin {
  public async handle(
    { auth, response }: HttpContextContract,
    next: () => Promise<void>
  ) {
    if (auth.user?.role !== 'admin') {
      return response.forbidden({ message: 'Access denied' })
    }
    
    await next()
  }
}

Registering Middleware

 
typescript
// start/kernel.ts
// Global middleware
Server.middleware.register([
  () => import('@ioc:Adonis/Core/BodyParser'),
])

// Named middleware
Server.middleware.registerNamed({
  auth: () => import('App/Middleware/Auth'),
  admin: () => import('App/Middleware/CheckAdmin'),
})

Views & Templates

Edge Templates

 
typescript
// Rendering a view
public async index({ view }: HttpContextContract) {
  return view.render('posts/index', {
    posts: await Post.all(),
  })
}

Template Example

 
html
<!-- resources/views/posts/index.edge -->
@layout('layouts/main')

@section('content')
  <h1>Blog Posts</h1>
  
  @each(post in posts)
    <div class="post">
      <h2>{{ post.title }}</h2>
      <p>{{ excerpt(post.content, 100) }}</p>
      <a href="{{ route('posts.show', { id: post.id }) }}">Read more</a>
    </div>
  @endeach
  
  @if(posts.length === 0)
    <p>No posts found</p>
  @endif
@endsection

Edge Directives

 
html
<!-- Conditional rendering -->
@if(user.isAdmin)
  <span class="badge">Admin</span>
@elseif(user.isModerator)
  <span class="badge">Moderator</span>
@else
  <span class="badge">User</span>
@endif

<!-- Loops -->
@each(item in items)
  <li>{{ item.name }}</li>
@endeach

<!-- Including partials -->
@include('partials/header')

<!-- Components -->
@component('components/alert', { type: 'error' })
  <p>Something went wrong!</p>
@endcomponent

<!-- Layouts -->
@layout('layouts/main')
@section('content')
  <!-- Content here -->
@endsection

File Storage

Configuration

 
typescript
// config/drive.ts
export const driveConfig = {
  disk: Env.get('DRIVE_DISK'),

  disks: {
    local: {
      driver: 'local',
      root: Application.tmpPath('uploads'),
    },
    s3: {
      driver: 's3',
      key: Env.get('S3_KEY'),
      secret: Env.get('S3_SECRET'),
      bucket: Env.get('S3_BUCKET'),
      region: Env.get('S3_REGION'),
    },
  },
}

File Uploads

 
typescript
public async store({ request, response }: HttpContextContract) {
  // Single file upload
  const avatar = request.file('avatar')
  
  if (avatar) {
    await avatar.moveToDisk('/')
    // or with custom name
    await avatar.moveToDisk('/', {
      name: `${new Date().getTime()}.${avatar.extname}`,
    })
  }
  
  // Multiple files
  const images = request.files('images')
  
  for (const image of images) {
    await image.moveToDisk('/')
  }
  
  return response.created({ message: 'Files uploaded' })
}

File Operations

 
typescript
import Drive from '@ioc:Adonis/Core/Drive'

// Check if file exists
const exists = await Drive.exists('avatar.jpg')

// Get file URL (for S3 and other remote disks)
const url = await Drive.getUrl('avatar.jpg')

// Get signed URL
const signedUrl = await Drive.getSignedUrl('avatar.jpg', {
  expiresIn: '30mins',
})

// Delete file
await Drive.delete('avatar.jpg')

Common Challenges & Solutions

CORS Configuration

 
typescript
// config/cors.ts
export const corsConfig = {
  enabled: true,
  origin: true,
  methods: ['GET', 'HEAD', 'POST', 'PUT', 'DELETE'],
  headers: true,
  exposeHeaders: [
    'cache-control',
    'content-language',
    'content-type',
    'expires',
    'last-modified',
    'pragma',
  ],
  credentials: true,
  maxAge: 90,
}

Error Handling

 
typescript
// app/Exceptions/Handler.ts
import Logger from '@ioc:Adonis/Core/Logger'
import HttpExceptionHandler from '@ioc:Adonis/Core/HttpExceptionHandler'

export default class ExceptionHandler extends HttpExceptionHandler {
  protected statusPages = {
    '403': 'errors/forbidden',
    '404': 'errors/not-found',
    '500..599': 'errors/server-error',
  }

  constructor() {
    super(Logger)
  }

  public async handle(error, ctx) {
    // Custom error handling logic
    if (error.code === 'E_VALIDATION_FAILURE') {
      return ctx.response.status(422).send(error.messages)
    }
    
    // Fall back to default handler
    return super.handle(error, ctx)
  }
}

Rate Limiting

 
typescript
// Middleware for rate limiting
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
import { SimpleLimiter } from '@ioc:Adonis/Addons/RateLimiter'

export default class RateLimit {
  public async handle(
    { request, response }: HttpContextContract,
    next: () => Promise<void>
  ) {
    // 10 requests per minute
    const limiter = new SimpleLimiter('global', {
      duration: 60 * 1000,
      requests: 10,
    })
    
    try {
      await limiter.consume(request.ip())
      await next()
    } catch (error) {
      return response
        .status(429)
        .send({ message: 'Too many requests' })
    }
  }
}

Best Practices

Environment Variables

 
typescript
// .env
PORT=3333
HOST=0.0.0.0
NODE_ENV=development
APP_KEY=your-secret-key
DB_CONNECTION=sqlite
DB_NAME=adonis

// Using Env in code
import Env from '@ioc:Adonis/Core/Env'

const dbName = Env.get('DB_NAME')
const port = Env.get('PORT', 3333) // With default

Dependency Injection

 
typescript
// Register a binding
import { ApplicationContract } from '@ioc:Adonis/Core/Application'

export default class AppProvider {
  constructor(protected app: ApplicationContract) {}

  public register() {
    this.app.singleton('App/Services/NotificationService', () => {
      return new NotificationService()
    })
  }
}

// Using the binding
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'

export default class UsersController {
  public async notify({ container }: HttpContextContract) {
    const notification = container.use('App/Services/NotificationService')
    await notification.send()
  }
}

Command Line Tasks

 
bash
# Create a custom command
node ace make:command SendDailyEmails
 
typescript
// app/Commands/SendDailyEmails.ts
import { BaseCommand } from '@adonisjs/core/build/standalone'

export default class SendDailyEmails extends BaseCommand {
  public static commandName = 'send:emails'
  public static description = 'Send daily digest emails to users'

  public static settings = {
    loadApp: true,
  }

  public async run() {
    this.logger.info('Sending emails')
    // Email sending logic here
    this.logger.success('Emails sent successfully')
  }
}

// Running the command
// node ace send:emails

Logging

 
typescript
import Logger from '@ioc:Adonis/Core/Logger'

// Log levels
Logger.info('Application started')
Logger.warn('Deprecated feature used')
Logger.error('Something went wrong', { error })
Logger.debug('Debug information')

// In controllers
public async index({ logger }: HttpContextContract) {
  logger.info('Listing users')
}

Testing

Configuration

 
bash
# Install test dependencies
npm i -D @japa/runner @japa/preset-adonis

# Create test
node ace make:test user

Unit Test Example

 
typescript
// tests/unit/user.spec.ts
import { test } from '@japa/runner'
import User from 'App/Models/User'

test.group('User', () => {
  test('create user', async ({ assert }) => {
    const user = await User.create({
      email: 'test@example.com',
      password: 'password',
    })
    
    assert.equal(user.email, 'test@example.com')
  })
})

Functional Test Example

 
typescript
// tests/functional/auth.spec.ts
import { test } from '@japa/runner'
import Database from '@ioc:Adonis/Lucid/Database'

test.group('Authentication', (group) => {
  group.each.setup(async () => {
    await Database.beginGlobalTransaction()
    return () => Database.rollbackGlobalTransaction()
  })

  test('login with valid credentials', async ({ client, assert }) => {
    // Create a user
    await client
      .post('/register')
      .form({
        email: 'test@example.com',
        password: 'password',
        password_confirmation: 'password',
      })

    // Attempt login
    const response = await client
      .post('/login')
      .form({
        email: 'test@example.com',
        password: 'password',
      })
    
    response.assertStatus(200)
    response.assertRedirectsTo('/dashboard')
  })
})

Running Tests

 
bash
# Run all tests
node ace test

# Run specific test
node ace test tests/functional/auth.spec.ts

Resources for Further Learning

Official Resources

Packages & Extensions

Tutorials & Courses

Comparison with Other Frameworks

FeatureAdonisJSExpressNestJS
ArchitectureFull-stack MVCMinimalist, unopinionatedModular, Angular-inspired
Out-of-box featuresHigh (ORM, Auth, etc.)Low (requires plugins)Medium (DI, modules)
TypeScript supportNativeAdd-onNative
Learning curveMediumLowHigh
Database handlingLucid ORM built-inNo built-in ORMTypeORM/Mongoose integration
TestingBuilt-in test frameworkNo built-in testingJest integration
Community sizeMediumVery largeLarge
Best forFull-stack applicationsLightweight APIsEnterprise applications
 
 
Scroll to Top