Stacks Actions
Server actions are reusable business logic units invoked from routes, events, or CLI commands.
Key Paths
- Core package:
storage/framework/core/actions/src/
- Application actions:
app/Actions/
- Default framework actions:
storage/framework/defaults/app/Actions/
- Framework actions (generated):
storage/framework/actions/
Creating an Action
// app/Actions/CreateWidget.ts
import { Action } from '@stacksjs/actions'
import { toSnakeCaseKeys } from '@stacksjs/orm'
import { response } from '@stacksjs/router'
export default new Action({
name: 'Create Widget',
description: 'Create a widget',
method: 'POST',
model: Widget,
async handle(request: RequestInstance) {
await request.validate()
const widget = await Widget.create(toSnakeCaseKeys(request.all()))
return response.json(widget, 201)
},
})
Use the Action class, an explicit HTTP method, and response helpers. Store and
update actions should set model and call request.validate() before persisting
input. Import framework helpers explicitly, following the default actions.
Resource Action Contract
Show, update, and destroy actions must distinguish malformed identifiers,
missing records, invalid input, and operational failures:
- Read resource identifiers from
request.getParam('id'), never from the
request body.
- Convert the value to a number and require a safe positive integer. Return
422 when it is malformed.
- Validate update input with the action model, then normalize persisted keys
with
toSnakeCaseKeys(request.all()) when the service expects database
column names.
- Core update services return
undefined when the row does not exist. Core
destroy services return false. They throw only for invalid domain input,
conflicts, or real operational failures.
- Return
404 when show or update returns undefined, or destroy returns
false. Never return a successful null, an unconditional 204, or a
generic 500 for an absent row.
- Preserve domain status codes such as
409 for uniqueness conflicts and
422 for relationship or state validation.
Share identifier and not-found response helpers inside a domain instead of
copying the contract across every resource. The built-in Commerce actions use
Actions/Commerce/commerce-action.ts as the reference implementation.
Auto-Generated API Actions (useApi Trait)
When a model defines useApi, the framework auto-generates REST actions:
defineModel({
name: 'Product',
traits: {
useApi: {
uri: 'products',
routes: ['index', 'store', 'show', 'update', 'destroy']
}
}
})
This generates:
GET /api/products → Index action (list all)
POST /api/products → Store action (create)
GET /api/products/{id} → Show action (get one)
PUT /api/products/{id} → Update action
DELETE /api/products/{id} → Destroy action
Default Framework Actions (80+)
Authentication Actions
LoginAction — POST /login (validates email + password, returns token + user)
RegisterAction — POST /register
LogoutAction — POST /logout (auth required)
RefreshTokenAction — POST /auth/refresh
CreateTokenAction — POST /auth/token
ListTokensAction — GET /auth/tokens (auth required)
RevokeTokenAction — DELETE /auth/tokens/{id}
GetMeAction — GET /me (auth required)
PasskeyRegistrationAction — passkey authentication options
Dashboard Settings Actions (40+)
For each settings category (AI, Analytics, App, Cache, Cloud, Database, DNS, Email, Environment, FileSystems, Hashing, Library, Logging, Notifications, Payment, Ports, Queue, SearchEngine, Security, Services, Storage, Team, UI):
Get{Category}SettingsAction — read current settings
Update{Category}SettingsAction — update settings
Commerce Actions
- CRUD actions for: Products, Orders, Customers, Payments, Coupons, GiftCards, Reviews, Shipping, DeliveryRoutes, TaxRates, LicenseKeys, etc.
Content Actions
- CRUD actions for: Posts, Pages, Authors, Categories, Tags, Comments
System Actions
HealthAction — GET /health (returns status, uptime, memory, PID, Bun version)
GetUserCountAction — user count for dashboard
GetSubscriberCountAction — subscriber count
- Deployment CRUD actions
- Job monitoring actions
- Notification actions
- Request analytics actions
Action Handler Pattern
Actions receive the enhanced request object:
async handle(request: EnhancedRequest) {
const name = request.get('name') // input value
const email = request.input('email') // alias
const all = request.all() // all input
const user = await request.user() // authenticated user
return { success: true, data: { ... } }
}
Using Actions in Routes
// String-based (auto-loaded)
route.post('/users', 'Actions/CreateUser')
// In events (app/Events.ts)
{ 'user:registered': ['SendWelcomeEmail'] } // action name as listener
CLI Commands
buddy make:action [name] — scaffold a new action
Gotchas
- Application actions go in
app/Actions/
- Framework default actions are in
storage/framework/defaults/app/Actions/
- The
handle() method is required — it receives the request object
- Actions used as event listeners also have a
handle(event) method
- The
useApi model trait auto-generates CRUD actions + routes
- Actions are resolved dynamically at runtime via string names
- The HealthAction at
/health is useful for container health checks
- Login action returns
{ token: string, user: { id, email, name } }
- All dashboard settings actions read/write from the corresponding config files
1---2name: stacks-actions3description: Use when working with Stacks server actions - creating actions in app/Actions/, auto-generated API actions from the useApi model trait, the 80+ default framework actions (auth, dashboard, commerce, content, deployment, jobs), action request/response handling, or action registration. Covers @stacksjs/actions and storage/framework/defaults/app/Actions/.4license: MIT5---67# Stacks Actions89Server actions are reusable business logic units invoked from routes, events, or CLI commands.1011## Key Paths12- Core package: `storage/framework/core/actions/src/`13- Application actions: `app/Actions/`14- Default framework actions: `storage/framework/defaults/app/Actions/`15- Framework actions (generated): `storage/framework/actions/`1617## Creating an Action1819```typescript20// app/Actions/CreateWidget.ts21import { Action } from '@stacksjs/actions'22import { toSnakeCaseKeys } from '@stacksjs/orm'23import { response } from '@stacksjs/router'2425export default new Action({26 name: 'Create Widget',27 description: 'Create a widget',28 method: 'POST',29 model: Widget,3031 async handle(request: RequestInstance) {32 await request.validate()3334 const widget = await Widget.create(toSnakeCaseKeys(request.all()))3536 return response.json(widget, 201)37 },38})39```4041Use the `Action` class, an explicit HTTP method, and `response` helpers. Store and42update actions should set `model` and call `request.validate()` before persisting43input. Import framework helpers explicitly, following the default actions.4445## Resource Action Contract4647Show, update, and destroy actions must distinguish malformed identifiers,48missing records, invalid input, and operational failures:49501. Read resource identifiers from `request.getParam('id')`, never from the51 request body.522. Convert the value to a number and require a safe positive integer. Return53 `422` when it is malformed.543. Validate update input with the action model, then normalize persisted keys55 with `toSnakeCaseKeys(request.all())` when the service expects database56 column names.574. Core update services return `undefined` when the row does not exist. Core58 destroy services return `false`. They throw only for invalid domain input,59 conflicts, or real operational failures.605. Return `404` when show or update returns `undefined`, or destroy returns61 `false`. Never return a successful `null`, an unconditional `204`, or a62 generic `500` for an absent row.636. Preserve domain status codes such as `409` for uniqueness conflicts and64 `422` for relationship or state validation.6566Share identifier and not-found response helpers inside a domain instead of67copying the contract across every resource. The built-in Commerce actions use68`Actions/Commerce/commerce-action.ts` as the reference implementation.6970## Auto-Generated API Actions (useApi Trait)7172When a model defines `useApi`, the framework auto-generates REST actions:7374```typescript75defineModel({76 name: 'Product',77 traits: {78 useApi: {79 uri: 'products',80 routes: ['index', 'store', 'show', 'update', 'destroy']81 }82 }83})84```8586This generates:87- `GET /api/products` → Index action (list all)88- `POST /api/products` → Store action (create)89- `GET /api/products/{id}` → Show action (get one)90- `PUT /api/products/{id}` → Update action91- `DELETE /api/products/{id}` → Destroy action9293## Default Framework Actions (80+)9495### Authentication Actions96- `LoginAction` — POST /login (validates email + password, returns token + user)97- `RegisterAction` — POST /register98- `LogoutAction` — POST /logout (auth required)99- `RefreshTokenAction` — POST /auth/refresh100- `CreateTokenAction` — POST /auth/token101- `ListTokensAction` — GET /auth/tokens (auth required)102- `RevokeTokenAction` — DELETE /auth/tokens/{id}103- `GetMeAction` — GET /me (auth required)104- `PasskeyRegistrationAction` — passkey authentication options105106### Dashboard Settings Actions (40+)107For each settings category (AI, Analytics, App, Cache, Cloud, Database, DNS, Email, Environment, FileSystems, Hashing, Library, Logging, Notifications, Payment, Ports, Queue, SearchEngine, Security, Services, Storage, Team, UI):108- `Get{Category}SettingsAction` — read current settings109- `Update{Category}SettingsAction` — update settings110111### Commerce Actions112- CRUD actions for: Products, Orders, Customers, Payments, Coupons, GiftCards, Reviews, Shipping, DeliveryRoutes, TaxRates, LicenseKeys, etc.113114### Content Actions115- CRUD actions for: Posts, Pages, Authors, Categories, Tags, Comments116117### System Actions118- `HealthAction` — GET /health (returns status, uptime, memory, PID, Bun version)119- `GetUserCountAction` — user count for dashboard120- `GetSubscriberCountAction` — subscriber count121- Deployment CRUD actions122- Job monitoring actions123- Notification actions124- Request analytics actions125126## Action Handler Pattern127128Actions receive the enhanced request object:129```typescript130async handle(request: EnhancedRequest) {131 const name = request.get('name') // input value132 const email = request.input('email') // alias133 const all = request.all() // all input134 const user = await request.user() // authenticated user135136 return { success: true, data: { ... } }137}138```139140## Using Actions in Routes141142```typescript143// String-based (auto-loaded)144route.post('/users', 'Actions/CreateUser')145146// In events (app/Events.ts)147{ 'user:registered': ['SendWelcomeEmail'] } // action name as listener148```149150## CLI Commands151- `buddy make:action [name]` — scaffold a new action152153## Gotchas154- Application actions go in `app/Actions/`155- Framework default actions are in `storage/framework/defaults/app/Actions/`156- The `handle()` method is required — it receives the request object157- Actions used as event listeners also have a `handle(event)` method158- The `useApi` model trait auto-generates CRUD actions + routes159- Actions are resolved dynamically at runtime via string names160- The HealthAction at `/health` is useful for container health checks161- Login action returns `{ token: string, user: { id, email, name } }`162- All dashboard settings actions read/write from the corresponding config files