Skip to content

Instantly share code, notes, and snippets.

@psgganesh
Created June 22, 2025 05:42
Show Gist options
  • Save psgganesh/8889803ee40a8cb6d0e5b5bd97b69e26 to your computer and use it in GitHub Desktop.
Save psgganesh/8889803ee40a8cb6d0e5b5bd97b69e26 to your computer and use it in GitHub Desktop.
adonis-js-v6-quick-rundown

This one-pager guide outlines how to work with AdonisJS routes, resource controllers, migrations, models, DTOs, and actions, leveraging the adocasts/package-actions and adocasts/package-dto packages.


AdonisJS Development Workflow

1. Setup & Installation

First, ensure you have an AdonisJS project set up. If not:

npm init adonis-ts-app@latest my-app
cd my-app
npm install

Install the DTO and Actions packages:

npm install @adocasts/dto @adocasts/actions
node ace configure @adocasts/dto

2. Migrations and Models

Migrations define your database schema, and Models interact with it.

  • Create a Migration and Model:

    node ace make:model User -m

    This command creates app/Models/User.ts (the model) and database/migrations/TIMESTAMP_users.ts (the migration).

  • Define Schema in Migration: Open database/migrations/TIMESTAMP_users.ts.

    import BaseSchema from '@ioc:Adonis/Lucid/Schema'
    
    export default class UsersSchema 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('name', 255).nullable()
          table.timestamp('created_at', { useTz: true }).notNullable()
          table.timestamp('updated_at', { useTz: true }).notNullable()
        })
      }
    
      public async down () {
        this.schema.dropTable(this.tableName)
      }
    }
  • Define Model Properties: Open app/Models/User.ts.

    import { DateTime } from 'luxon'
    import { BaseModel, column } from '@ioc:Adonis/Lucid/Orm'
    
    export default class User extends BaseModel {
      @column({ isPrimary: true })
      public id: number
    
      @column()
      public email: string
    
      @column({ serializeAs: null }) // Don't expose password
      public password: string
    
      @column()
      public name: string | null
    
      @column.dateTime({ autoCreate: true })
      public createdAt: DateTime
    
      @column.dateTime({ autoCreate: true, autoUpdate: true })
      public updatedAt: DateTime
    }
  • Run Migrations:

    node ace migration:run

3. DTOs (@adocasts/package-dto)

DTOs (Data Transfer Objects) define the shape of data sent between layers, ensuring type safety and explicit contracts, especially for frontend consumption.

  • Generate DTO from Model:

    node ace make:dto User

    This creates app/Dtos/UserDto.ts.

  • Customize User DTO (app/Dtos/UserDto.ts):

    import { BaseModelDto } from '@adocasts/dto'
    import User from 'App/Models/User'
    
    export default class UserDto extends BaseModelDto {
      public id: number
      public email: string
      public name: string | null
      public createdAt: string // Often converted to string for frontend
      public updatedAt: string // Often converted to string for frontend
    
      constructor(user: User) {
        super()
        this.id = user.id
        this.email = user.email
        this.name = user.name
        this.createdAt = user.createdAt.toISO() // Convert Luxon DateTime to ISO string
        this.updatedAt = user.updatedAt.toISO() // Convert Luxon DateTime to ISO string
      }
    }

    Note: Ensure your package.json has the import alias for DTOs:

    "imports": {
      "#dtos/*": "./app/dtos/*.js"
    }

4. Actions (@adocasts/package-actions)

Actions encapsulate specific business logic, making it reusable and testable, often taking a DTO as input.

  • Create an Action:

    node ace make:action CreateUser

    This creates app/Actions/CreateUser.ts.

  • Define an Action (app/Actions/CreateUser.ts):

    import { BaseAction } from '@adocasts/actions'
    import User from 'App/Models/User'
    import CreateUserDto from '#dtos/CreateUserDto' // We'll create this DTO
    
    export default class CreateUser extends BaseAction {
      public async handle(data: CreateUserDto): Promise<User> {
        const user = await User.create({
          email: data.email,
          password: data.password, // Remember to hash passwords in a real app!
          name: data.name,
        })
        return user
      }
    }
  • Create the CreateUserDto (app/Dtos/CreateUserDto.ts): This DTO will define the input shape for our CreateUser action.

    import { BaseDto } from '@adocasts/dto'
    import { IsEmail, IsString, IsOptional, MinLength } from 'class-validator' // Example validation
    
    export default class CreateUserDto extends BaseDto {
      @IsEmail()
      public email: string
    
      @IsString()
      @MinLength(8)
      public password: string
    
      @IsOptional()
      @IsString()
      public name?: string
    }

    Note: You might need to install class-validator and reflect-metadata and configure tsconfig.json for DTO validation.

5. Resource Controllers

Resource controllers provide a conventional way to handle CRUD operations for a resource.

  • Create a Resource Controller:

    node ace make:controller UsersController

    This creates app/Controllers/Http/UsersController.ts.

  • Implement Controller with DTOs and Actions (app/Controllers/Http/UsersController.ts):

    import type { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
    import User from 'App/Models/User'
    import UserDto from '#dtos/UserDto'
    import CreateUserDto from '#dtos/CreateUserDto'
    import UpdateUserDto from '#dtos/UpdateUserDto' // We'll create this
    import CreateUser from 'App/Actions/CreateUser'
    import { inject } from '@adonisjs/core/build/standalone' // For action injection
    
    @inject()
    export default class UsersController {
      constructor(protected createUserAction: CreateUser) {} // Inject the action
    
      public async index({ response }: HttpContextContract) {
        const users = await User.all()
        return response.ok(UserDto.fromArray(users)) // Use fromArray helper
      }
    
      public async store({ request, response }: HttpContextContract) {
        const payload = await request.validate(CreateUserDto) // Validate input with DTO
    
        // Use the injected action
        const user = await this.createUserAction.handle(payload)
    
        return response.created(new UserDto(user))
      }
    
      public async show({ params, response }: HttpContextContract) {
        const user = await User.findOrFail(params.id)
        return response.ok(new UserDto(user))
      }
    
      public async update({ params, request, response }: HttpContextContract) {
        const user = await User.findOrFail(params.id)
        const payload = await request.validate(UpdateUserDto) // Validate input with DTO
    
        user.merge(payload) // Merge DTO data into model
        await user.save()
    
        return response.ok(new UserDto(user))
      }
    
      public async destroy({ params, response }: HttpContextContract) {
        const user = await User.findOrFail(params.id)
        await user.delete()
        return response.noContent()
      }
    }
  • Create the UpdateUserDto (app/Dtos/UpdateUserDto.ts):

    import { BaseDto } from '@adocasts/dto'
    import { IsEmail, IsString, IsOptional, MinLength } from 'class-validator'
    
    export default class UpdateUserDto extends BaseDto {
      @IsOptional()
      @IsEmail()
      public email?: string
    
      @IsOptional()
      @IsString()
      @MinLength(8)
      public password?: string
    
      @IsOptional()
      @IsString()
      public name?: string
    }

6. Routes

Define API endpoints that map to your resource controller.

  • Define Resource Routes (start/routes.ts):
    import Route from '@ioc:Adonis/Core/Route'
    
    // Define resourceful routes for the UsersController
    Route.resource('users', 'UsersController').apiOnly()
    • .apiOnly(): This modifier restricts the resource routes to only include typical API endpoints (index, store, show, update, destroy), excluding create (form to create) and edit (form to edit), which are generally not needed for API-only applications.

Summary of Flow:

  1. Migration: Defines the database table structure (users table).
  2. Model: Provides an ORM interface to interact with the users table.
  3. DTOs:
    • CreateUserDto: Defines the expected input shape for creating a user (e.g., from a request body).
    • UpdateUserDto: Defines the expected input shape for updating a user.
    • UserDto: Defines the output shape of a user, typically hiding sensitive fields (like password) and formatting data for the consumer.
  4. Action: CreateUser encapsulates the business logic for creating a user, accepting CreateUserDto and returning a User model. This keeps your controller lean.
  5. Resource Controller: UsersController handles HTTP requests for user-related operations.
    • It uses request.validate() with DTOs for input validation and strong typing.
    • It delegates complex business logic to "Actions" (e.g., createUserAction.handle).
    • It transforms Lucid Models into DTOs before sending responses, ensuring consistent output.
  6. Routes: Route.resource('users', 'UsersController').apiOnly() conveniently registers all standard RESTful endpoints for the UsersController, making your routing clean and conventional.

This setup promotes a clean, maintainable, and type-safe AdonisJS application by separating concerns and leveraging the provided packages.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment