| name | adonisjs-enums |
| description | TypeScript string enums with labels and business logic for AdonisJS v6. Use when working with enums, status values, fixed sets of options, or when the user mentions enums, status enums, enum cases, or finite value sets. Also use when typing Lucid model columns or validating enum fields with VineJS. |
AdonisJS Enums
Enums provide type-safe, finite sets of values across the entire stack — models, validators, actions, and controllers.
Related guides:
- DTOs - DTO properties typed as enums
- Actions - Business logic that references enums
- Lucid Models - Typing model columns with enums
- VineJS Enum - Validating enum values in request payloads
Always Use String Enums
Always use string enums — not numeric enums, not const enums, not plain union types.
String values are human-readable in the database, in logs, and over the wire. Numeric enums are opaque; const enums have ESM/isolation issues.
export enum OrderStatus {
Pending = 'pending',
Processing = 'processing',
Completed = 'completed',
Cancelled = 'cancelled',
}
export enum OrderStatus {
Pending,
Processing,
Completed,
}
export const enum OrderStatus {
Pending = 'pending',
}
type OrderStatus = 'pending' | 'processing' | 'completed'
File Naming & Location
AdonisJS v6 uses snake_case for all files.
app/enums/
├── order_status.ts
├── payment_method.ts
├── user_role.ts
└── queue.ts
export enum OrderStatus { ... }
Basic Enum
export enum OrderStatus {
Pending = 'pending',
Processing = 'processing',
Completed = 'completed',
Cancelled = 'cancelled',
}
Enum with Labels
Add a label() method for human-readable display text (UI selects, notifications, etc.):
export enum OrderStatus {
Pending = 'pending',
Processing = 'processing',
Completed = 'completed',
Cancelled = 'cancelled',
}
export const OrderStatusLabel: Record<OrderStatus, string> = {
[OrderStatus.Pending]: 'Pending',
[OrderStatus.Processing]: 'Processing',
[OrderStatus.Completed]: 'Completed',
[OrderStatus.Cancelled]: 'Cancelled',
}
export function getOrderStatusLabel(status: OrderStatus): string {
return OrderStatusLabel[status]
}
export function orderStatusOptions(): Array<{ value: string; label: string }> {
return Object.values(OrderStatus).map((value) => ({
value,
label: OrderStatusLabel[value],
}))
}
Usage:
getOrderStatusLabel(OrderStatus.Completed)
orderStatusOptions()
Enum with Business Logic
Enums can carry domain behaviour directly — keeps switch statements out of your actions:
export enum PaymentMethod {
Stripe = 'stripe',
PayPal = 'paypal',
BankTransfer = 'bank_transfer',
}
export const PaymentMethodLabel: Record<PaymentMethod, string> = {
[PaymentMethod.Stripe]: 'Credit / Debit Card',
[PaymentMethod.PayPal]: 'PayPal',
[PaymentMethod.BankTransfer]: 'Bank Transfer',
}
export function processingFee(method: PaymentMethod, amount: number): number {
switch (method) {
case PaymentMethod.Stripe:
return Math.round(amount * 0.029 + 30)
case PaymentMethod.PayPal:
return Math.round(amount * 0.034 + 30)
case PaymentMethod.BankTransfer:
return 0
}
}
export function supportsRefunds(method: PaymentMethod): boolean {
return method === PaymentMethod.Stripe || method === PaymentMethod.PayPal
}
For richer OO behaviour on a per-value basis, a small class with static factories or a plain Map<Enum, Config> are both clean alternatives to open-ended switch statements.
Enum with Colour / Icon Metadata
For UI-heavy applications, pair metadata records alongside the enum:
export enum OrderStatus {
Pending = 'pending',
Processing = 'processing',
Completed = 'completed',
Cancelled = 'cancelled',
}
export const OrderStatusColor: Record<OrderStatus, string> = {
[OrderStatus.Pending]: 'yellow',
[OrderStatus.Processing]: 'blue',
[OrderStatus.Completed]: 'green',
[OrderStatus.Cancelled]: 'red',
}
export const OrderStatusIcon: Record<OrderStatus, string> = {
[OrderStatus.Pending]: 'clock',
[OrderStatus.Processing]: 'refresh',
[OrderStatus.Completed]: 'check-circle',
[OrderStatus.Cancelled]: 'x-circle',
}
Usage in Lucid Models
Type the column property with the enum. Lucid stores and reads the raw string value — no special cast is needed for string enums.
import { BaseModel, column } from '@adonisjs/lucid/orm'
import { OrderStatus } from '#enums/order_status'
export default class Order extends BaseModel {
@column({ isPrimary: true })
declare id: number
@column()
declare status: OrderStatus
@column()
declare paymentMethod: PaymentMethod
}
For enum columns in migrations, use Object.values() to derive the allowed list at the database level:
import { BaseSchema } from '@adonisjs/lucid/schema'
import { OrderStatus } from '#enums/order_status'
export default class extends BaseSchema {
protected tableName = 'orders'
async up() {
this.schema.createTable(this.tableName, (table) => {
table.increments('id')
table.enum('status', Object.values(OrderStatus)).notNullable().defaultTo(OrderStatus.Pending)
table.timestamps(true)
})
}
async down() {
this.schema.dropTable(this.tableName)
}
}
Usage in VineJS Validators
Pass the TypeScript enum directly to vine.enum():
import vine from '@vinejs/vine'
import { OrderStatus } from '#enums/order_status'
import { PaymentMethod } from '#enums/payment_method'
export const createOrderValidator = vine.compile(
vine.object({
status: vine.enum(OrderStatus),
paymentMethod: vine.enum(PaymentMethod),
notes: vine.string().optional(),
})
)
export const updateOrderValidator = vine.compile(
vine.object({
status: vine.enum(OrderStatus).optional(),
})
)
VineJS infers the output type as the enum itself, so payload.status is typed as OrderStatus — no manual cast needed.
Usage in DTOs
import { OrderStatus } from '#enums/order_status'
import { PaymentMethod } from '#enums/payment_method'
export interface CreateOrderDto {
customerEmail: string
status: OrderStatus
paymentMethod: PaymentMethod
notes?: string
}
Usage in Actions
import Order from '#models/order'
import { OrderStatus } from '#enums/order_status'
import OrderException from '#exceptions/order_exception'
export default class CancelOrderAction {
async handle(order: Order): Promise<Order> {
if (order.status !== OrderStatus.Pending && order.status !== OrderStatus.Processing) {
throw OrderException.cannotCancel(order)
}
order.status = OrderStatus.Cancelled
await order.save()
return order
}
}
Common Patterns
Comparing Enum Values
if (order.status === OrderStatus.Completed) { ... }
const cancellable = [OrderStatus.Pending, OrderStatus.Processing]
if (cancellable.includes(order.status)) { ... }
Match / Switch with Exhaustiveness
function statusMessage(status: OrderStatus): string {
switch (status) {
case OrderStatus.Pending: return 'Your order is being prepared.'
case OrderStatus.Processing: return 'Your order is on its way.'
case OrderStatus.Completed: return 'Your order has arrived.'
case OrderStatus.Cancelled: return 'Your order was cancelled.'
default:
status satisfies never
return ''
}
}
Queue Enum
A common pattern — centralise queue names to avoid magic strings throughout jobs:
export enum Queue {
Default = 'default',
Emails = 'emails',
Notifications = 'notifications',
Processing = 'processing',
}
import { Queue } from '#enums/queue'
await someQueue.dispatch(job, { queue: Queue.Emails })
Directory Structure
app/enums/
├── order_status.ts
├── payment_method.ts
├── payment_provider.ts
├── user_role.ts
└── queue.ts
Group all enums flat under app/enums/. Sub-folders are only needed once the count becomes unwieldy.
Testing Enums
Enum logic (label helpers, fee calculations) is easy to test in isolation:
import { test } from '@japa/runner'
import { OrderStatus, getOrderStatusLabel, orderStatusOptions } from '#enums/order_status'
test.group('OrderStatus', () => {
test('returns the correct label', ({ assert }) => {
assert.equal(getOrderStatusLabel(OrderStatus.Completed), 'Completed')
assert.equal(getOrderStatusLabel(OrderStatus.Cancelled), 'Cancelled')
})
test('returns dropdown options for all values', ({ assert }) => {
const options = orderStatusOptions()
assert.lengthOf(options, Object.values(OrderStatus).length)
assert.deepInclude(options, { value: 'pending', label: 'Pending' })
})
})
When to Use Enums vs Plain Union Types
Use string enums when:
- The value persists to the database
- The value is validated in request payloads
- The value is compared in business logic across multiple files
- You need label, colour, or icon metadata alongside the value
Use a plain union type when:
- The set of values is local to a single function or type
- No runtime iteration is needed
- You're describing a pure TypeScript contract with no DB or validation surface
type SortDirection = 'asc' | 'desc'
Summary
String enums provide:
- Type safety and IDE autocomplete across the full stack
- Readable values in the database and logs
- A single source of truth for label, colour, and icon metadata
- Direct compatibility with
vine.enum() and Lucid column types
- Exhaustiveness checking in switch statements
Always use string-backed enums — never numeric enums, never const enums for shared code.