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.
First, ensure you have an AdonisJS project set up. If not:
npm init adonis-ts-app@latest my-app
cd my-app
npm installInstall the DTO and Actions packages:
npm install @adocasts/dto @adocasts/actions
node ace configure @adocasts/dtoMigrations 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) anddatabase/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
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.jsonhas the import alias for DTOs:"imports": { "#dtos/*": "./app/dtos/*.js" }
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 ourCreateUseraction.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-validatorandreflect-metadataand configuretsconfig.jsonfor DTO validation.
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 }
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), excludingcreate(form to create) andedit(form to edit), which are generally not needed for API-only applications.
- Migration: Defines the database table structure (
userstable). - Model: Provides an ORM interface to interact with the
userstable. - 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.
- Action:
CreateUserencapsulates the business logic for creating a user, acceptingCreateUserDtoand returning aUsermodel. This keeps your controller lean. - Resource Controller:
UsersControllerhandles 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.
- It uses
- Routes:
Route.resource('users', 'UsersController').apiOnly()conveniently registers all standard RESTful endpoints for theUsersController, 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.