// Get specific keys only const credentials = request.only<{ email: string; password: string }>([ 'email', 'password', ])
// Get all except specific keys const dataWithoutPassword = request.except<{ name: string; email: string }>([ 'password', ])
// Check if key exists if (request.has('remember')) { // ... }
// Check if multiple keys exist if (request.has(['email', 'password'])) { // ... }
// Check if any of the keys exist if (request.hasAny(['email', 'username'])) { // ... }
// Check if key exists and is not empty if (request.filled('name')) { // ... }
// Check if key is missing if (request.missing('optional_field')) { // ... } }
### Type-Safe Input Methods
```typescript
async handle(request: Request) {
// String input
const title = request.string('title', '')
// Integer input
const page = request.integer('page', 1)
// Float input
const price = request.float('price', 0.0)
// Boolean input
const active = request.boolean('active', false)
// Array input
const tags = request.array<string>('tags')
}
Route Parameters
Access route parameters:
// Route: /users/{id}/posts/{postId}
async handle(request: Request) {
const userId = request.params.id
const postId = request.params.postId
// Or using get method
const id = request.get('id')
}
File Uploads
Handle file uploads:
async handle(request: Request) {
// Get single file
const avatar = request.file('avatar')
if (avatar) {
// Store file
const path = await avatar.store('avatars')
// Store with custom name
const customPath = await avatar.storeAs('avatars', 'custom-name.jpg')
// Get file properties
console.log(avatar.name) // Original filename
console.log(avatar.size) // File size in bytes
console.log(avatar.type) // MIME type
}
// Check if file exists
if (request.hasFile('document')) {
const document = request.file('document')
}
// Get multiple files
const images = request.getFiles('images')
for (const image of images) {
await image.store('gallery')
}
// Get all files
const allFiles = request.allFiles()
}
Authentication
Access authenticated user:
async handle(request: Request) {
// Get authenticated user (set by auth middleware)
const user = await request.user()
if (!user) {
return response.unauthorized('Please log in')
}
// Get current access token
const token = await request.userToken()
// Check token abilities
if (await request.tokenCan('posts:create')) {
// User can create posts
}
if (await request.tokenCant('admin')) {
return response.forbidden('Admin access required')
}
}
Validation
Defining Validation Rules
import { schema } from '@stacksjs/validation'
export default new Action({
validations: {
// String validation
name: {
rule: schema.string().min(2).max(100),
message: {
min: 'Name is too short',
max: 'Name is too long',
},
},
// Email validation
email: {
rule: schema.string().email(),
message: 'Invalid email address',
},
// Number validation
age: {
rule: schema.number().min(18).max(120),
message: {
min: 'Must be at least 18',
max: 'Invalid age',
},
},
// Enum validation
status: {
rule: schema.enum(['active', 'inactive', 'pending']),
message: 'Invalid status value',
},
// Boolean validation
acceptTerms: {
rule: schema.boolean(),
message: 'You must accept the terms',
},
// Optional field
nickname: {
rule: schema.string().optional(),
},
},
async handle(request) {
// Validation runs automatically before handle()
// If validation fails, returns 422 with errors
},
})
Validation Response
When validation fails, the action returns a 422 response:
{
"error": "Validation failed",
"errors": {
"email": ["Invalid email address"],
"password": ["Password must be at least 8 characters"]
}
}
Response Helpers
JSON Response
import { response } from '@stacksjs/router'
async handle(request) {
// Success response
return response.json({
success: true,
data: { id: 1, name: 'John' },
})
// With status code
return response.json({ created: true }, 201)
}
Text Response
async handle() {
return response.text('Hello, World!')
}
Error Responses
import { response } from '@stacksjs/router'
async handle(request) {
// 401 Unauthorized
return response.unauthorized('Please log in')
// 403 Forbidden
return response.forbidden('Access denied')
// 404 Not Found
return response.notFound('Resource not found')
// 422 Validation Error
return response.validationError({
email: ['Email is required'],
})
// 500 Server Error
return response.serverError('Something went wrong')
}
Custom Response
async handle() {
return new Response(JSON.stringify({ custom: true }), {
status: 200,
headers: {
'Content-Type': 'application/json',
'X-Custom-Header': 'value',
},
})
}
Action Composition
Calling Other Actions
import { runAction } from '@stacksjs/actions'
export default new Action({
name: 'Create Order',
async handle(request) {
// Create the order
const order = await Order.create(request.all())
// Run another action
await runAction('SendOrderConfirmation', { orderId: order.id })
// Or import and call directly
const notifyAction = await import('./NotifyCustomerAction')
await notifyAction.default.handle({ customerId: order.customerId })
return response.json({ order })
},
})
Shared Logic
Extract shared logic into helper functions:
// app/Actions/helpers/validation.ts
export async function validateUser(userId: number): Promise<User | null> {
const user = await User.find(userId)
if (!user) {
throw new HttpError(404, 'User not found')
}
return user
}
// app/Actions/User/UpdateUserAction.ts
import { validateUser } from '../helpers/validation'
export default new Action({
async handle(request) {
const user = await validateUser(request.params.id)
await user.update(request.all())
return response.json({ user })
},
})
Dependency Injection
Using Services
import { Action } from '@stacksjs/actions'
import { PaymentService } from '@/services/PaymentService'
import { EmailService } from '@/services/EmailService'
export default new Action({
name: 'Process Payment',
async handle(request) {
const paymentService = new PaymentService()
const emailService = new EmailService()
const payment = await paymentService.process({
amount: request.get('amount'),
currency: request.get('currency'),
})
await emailService.sendReceipt(payment)
return response.json({ payment })
},
})
Configuration Access
import { config } from '@stacksjs/config'
export default new Action({
async handle() {
const apiKey = config.services.stripe.key
const appName = config.app.name
// Use configuration
},
})
Async Actions
Long-Running Operations
export default new Action({
name: 'Generate Report',
async handle(request) {
// Start async operation
const reportId = await Report.create({
status: 'processing',
userId: request.get('userId'),
})
// Dispatch to queue for background processing
await job('GenerateReport', { reportId }).dispatch()
// Return immediately
return response.json({
message: 'Report generation started',
reportId,
})
},
})
Streaming Responses
export default new Action({
name: 'Stream Data',
async handle(request) {
const stream = new ReadableStream({
async start(controller) {
for (let i = 0; i < 100; i++) {
controller.enqueue(`data: ${JSON.stringify({ count: i })}\n\n`)
await new Promise(resolve => setTimeout(resolve, 100))
}
controller.close()
},
})
return new Response(stream, {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
},
})
},
})
Action Middleware
Route-Level Middleware
Apply middleware when routing to an action:
// routes/api.ts
route.post('/admin/settings', 'Actions/Admin/UpdateSettingsAction')
.middleware('auth')
.middleware('abilities:admin')
Action-Based Authorization
Check permissions within the action:
export default new Action({
async handle(request) {
const user = await request.user()
if (!user) {
return response.unauthorized()
}
if (!await request.tokenCan('settings:update')) {
return response.forbidden('Missing required permission')
}
// Proceed with action
},
})
Error Handling
Throwing HTTP Errors
import { HttpError } from '@stacksjs/error-handling'
export default new Action({
async handle(request) {
const user = await User.find(request.params.id)
if (!user) {
throw new HttpError(404, 'User not found')
}
if (user.isDeleted) {
throw new HttpError(410, 'User has been deleted')
}
return response.json({ user })
},
})
Try-Catch Pattern
import { handleError } from '@stacksjs/error-handling'
export default new Action({
async handle(request) {
try {
const result = await riskyOperation()
return response.json({ result })
} catch (error) {
handleError(error)
return response.serverError('Operation failed')
}
},
})
Edge Cases and Gotchas
Request Body Consumption
The request body can only be read once. Stacks handles this automatically, but be aware when using raw request methods:
async handle(request) {
// Use request.all() or request.get() instead of request.json()
const data = request.all()
// Don't do this:
// const body = await request.json() // May fail if already consumed
}
Async Validation
Validation runs synchronously before handle(). For async validation (like uniqueness checks), validate within the handler:
async handle(request) {
const email = request.get('email')
// Async uniqueness check
const existing = await User.where('email', email).first()
if (existing) {
return response.validationError({
email: ['Email already in use'],
})
}
// Proceed with creation
}
File Upload Limits
Configure file upload limits in your server configuration:
// config/server.ts
export default {
maxRequestBodySize: 50 * 1024 * 1024, // 50MB
}
API Reference
Action Class
class Action {
constructor(options: ActionOptions)
name?: string
description?: string
method?: string
path?: string
validations?: ActionValidations
rate?: string
tries?: number
backoff?: number
enabled?: boolean
model?: string
handle: (request?: Request) => Promise<any> | any
}
Request Methods
| Method | Description |
|---|---|
get(key, default?) | Get input value |
all() | Get all input |
only(keys) | Get specific keys |
except(keys) | Exclude specific keys |
has(key) | Check key exists |
hasAny(keys) | Check any key exists |
filled(key) | Check key is not empty |
missing(key) | Check key is missing |
string(key, default?) | Get as string |
integer(key, default?) | Get as integer |
float(key, default?) | Get as float |
boolean(key, default?) | Get as boolean |
array(key) | Get as array |
file(key) | Get uploaded file |
hasFile(key) | Check file exists |
user() | Get authenticated user |
tokenCan(ability) | Check token ability |
Related Documentation
- Routing - Route to action mapping
- Middleware - Request middleware
- Validation - Validation rules
- Jobs - Background job processing
- Error Handling - Error handling