Appearance
Action Endpoints
Actions are custom API endpoints that go beyond standard CRUD operations. They allow you to implement business logic, workflows, and complex operations.
Types of Actions
Quickback supports two types of actions:
| Type | Route Pattern | Description |
|---|---|---|
| Record-based | {METHOD} /:id/:actionName | Operates on a specific record |
| Standalone | Custom path | Independent endpoint, no record required |
Default method is POST. Actions can use GET, POST, PUT, PATCH, or DELETE.
Record-Based Actions
Record-based actions operate on an existing record. The record is loaded and validated before your action executes.
Endpoint Format
POST /rooms/:id/activate # Default method
POST /invoices/:id/approve
GET /orders/:id/status # GET for read-only actions
DELETE /items/:id/archiveExample Request
POST /invoices/inv_123/approve
Content-Type: application/json
{
"notes": "Approved for Q1 budget"
}Example Response
json
{
"data": {
"id": "inv_123",
"status": "approved",
"approvedBy": "user_456",
"approvedAt": "2024-01-15T14:30:00Z"
}
}How It Works
- Authentication: User token is validated
- Record Loading: The record is fetched by ID
- Firewall Check: Ensures user can access this record
- Guard Check: Validates preconditions (e.g., status must be "pending")
- Input Validation: Request body is validated against Zod schema
- Execution: Your action handler runs
- Response: Result is returned to client
Preconditions
Actions can specify record conditions that must be met:
typescript
guard: {
roles: ["admin", "finance"],
record: { status: { equals: "pending" } } // Precondition
}If the precondition fails, the API returns a 403 Forbidden error.
Standalone Actions
Standalone actions are independent endpoints that don't require a record context.
Endpoint Format
POST /chat
GET /reports/summary
POST /webhooks/stripeExample Request
POST /chat
Content-Type: application/json
{
"message": "Hello, how can I help?"
}Example Response (Streaming)
For actions with responseType: 'stream':
data: {"type": "start"}
data: {"type": "chunk", "content": "Hello"}
data: {"type": "chunk", "content": "! I'm"}
data: {"type": "chunk", "content": " here to help."}
data: {"type": "done"}Defining Standalone Actions
typescript
export default defineActions(sessions, {
chat: {
standalone: true,
path: "/chat",
method: "POST",
responseType: "stream",
input: z.object({
message: z.string().min(1).max(2000),
}),
guard: {
roles: ["member", "admin"],
},
handler: "./handlers/chat",
},
});Input Validation
All actions use Zod schemas for input validation. Invalid requests return a 400 Bad Request error.
Example Schema
typescript
input: z.object({
notes: z.string().optional(),
amount: z.number().positive(),
dueDate: z.string().datetime(),
})Validation Error Response
json
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Invalid request data",
"details": [
{
"field": "amount",
"message": "Expected positive number"
}
]
}
}Response Types
Actions can return different response types:
JSON (default)
typescript
responseType: 'json'Standard JSON response:
json
{
"data": {
"success": true,
"result": { ... }
}
}Stream (Server-Sent Events)
typescript
responseType: 'stream'For real-time streaming responses (AI chat, live updates):
Content-Type: text/event-stream
data: {"chunk": "Hello"}
data: {"chunk": " world"}
data: [DONE]File Download
typescript
responseType: 'file'For file downloads (reports, exports):
Content-Type: application/pdf
Content-Disposition: attachment; filename="report.pdf"
<binary data>Access Control
Actions use the same guard system as CRUD operations:
Role-Based
typescript
guard: {
roles: ["admin", "manager"]
}Combined Conditions
typescript
guard: {
or: [
{ roles: ["admin"] },
{ roles: ["member"], record: { ownerId: { equals: "$ctx.userId" } } }
]
}Custom Guard Function
typescript
guard: async (ctx, record) => {
// Custom logic
return ctx.roles.includes('admin') || record.ownerId === ctx.userId;
}Error Handling
Standard Errors
| Status | Description |
|---|---|
400 | Invalid input / validation error |
401 | Not authenticated |
403 | Guard check failed (role or precondition) |
404 | Record not found (record-based actions) |
500 | Handler execution error |
Throwing Custom Errors
In your action handler:
typescript
execute: async ({ ctx, record, input }) => {
if (record.balance < input.amount) {
throw new ActionError('INSUFFICIENT_FUNDS', 'Not enough balance', 400);
}
// ... continue
}Protected Fields
Actions can modify fields that are protected from regular CRUD operations:
typescript
// In resource.ts
guards: {
protected: {
status: ["approve", "reject"], // Only these actions can modify status
amount: ["reviseAmount"],
}
}This allows the approve action to set status = "approved" even though the field is protected from regular PATCH requests.
Handler Files
For complex actions, use separate handler files:
Definition
typescript
// actions.ts
export default defineActions(invoices, {
generateReport: {
description: "Generate PDF report",
standalone: true,
path: "/invoices/report",
method: "GET",
responseType: "file",
input: z.object({
startDate: z.string().datetime(),
endDate: z.string().datetime(),
}),
guard: { roles: ["admin", "finance"] },
handler: "./handlers/generate-report",
},
});Handler Implementation
typescript
// handlers/generate-report.ts
import type { ActionExecutor } from '@quickback/types';
export const execute: ActionExecutor = async ({ db, ctx, input, services }) => {
const invoices = await db
.select()
.from(invoicesTable)
.where(between(invoicesTable.createdAt, input.startDate, input.endDate));
const pdf = await services.pdf.generate(invoices);
return {
file: pdf,
filename: `invoices-${input.startDate}-${input.endDate}.pdf`,
contentType: 'application/pdf',
};
};Executor Context
Action handlers receive these parameters:
typescript
interface ActionExecutorParams {
db: DrizzleDB; // Database instance
ctx: AppContext; // User context (userId, roles, orgId)
record?: TRecord; // The record (record-based only)
input: TInput; // Validated input from request
services: TServices; // Configured integrations
c: HonoContext; // Raw Hono context for advanced use
}