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 install
Install the DTO and Actions packages:
npm install @adocasts/dto @adocasts/actions
node ace configure @adocasts/dto
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) 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.json
has 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 ourCreateUser
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
andreflect-metadata
and configuretsconfig.json
for 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 (
users
table). - Model: Provides an ORM interface to interact with the
users
table. - 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:
CreateUser
encapsulates the business logic for creating a user, acceptingCreateUserDto
and returning aUser
model. This keeps your controller lean. - 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.
- 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.