Appearance
Actions - Custom Routes
Actions are custom business logic beyond CRUD. Two types: record-based and standalone.
Action Configuration
typescript
interface Action {
description: string;
input: z.ZodType<any>; // Zod schema for validation
guard: Guard; // Access control
// Inline execution
execute?: ActionExecutor;
// OR file reference (recommended for complex logic)
handler?: string;
// Standalone options
standalone?: boolean;
path?: string; // Custom path (standalone only)
method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
responseType?: 'json' | 'stream' | 'file';
}Guard Configuration
typescript
guard: {
roles?: string[]; // OR logic
record?: RecordConditions; // Preconditions (record-based only)
or?: Guard[];
and?: Guard[];
}
// Or use a function for complex logic
guard: async (ctx, record) => {
return ctx.roles.includes('admin') || record.ownerId === ctx.userId;
}HTTP Methods
Actions support all standard HTTP methods. Default is POST.
| Method | Input Source | Use Case |
|---|---|---|
GET | Query parameters | Read-only operations, fetching data |
POST | JSON body | Default, state-changing operations |
PUT | JSON body | Full replacement operations |
PATCH | JSON body | Partial updates |
DELETE | JSON body | Deletion with optional payload |
typescript
// GET action - input comes from query params
getStatus: {
method: "GET",
input: z.object({ format: z.string().optional() }),
// Called as: GET /invoices/:id/getStatus?format=detailed
}
// POST action (default) - input comes from JSON body
approve: {
// method: "POST" is implied
input: z.object({ notes: z.string().optional() }),
// Called as: POST /invoices/:id/approve with JSON body
}Record-Based Actions
Operates on an existing record. Route: {METHOD} /:id/{actionName}
typescript
// actions.ts
import { invoices } from '../../schema';
import { defineActions } from '@quickback/core';
import { eq } from 'drizzle-orm';
import { z } from 'zod';
export default defineActions(invoices, {
approve: {
description: "Approve invoice for payment",
input: z.object({
notes: z.string().optional(),
}),
guard: {
roles: ["admin", "finance"],
record: { status: { equals: "pending" } }, // Precondition
},
execute: async ({ db, ctx, record, input }) => {
const [updated] = await db
.update(invoices)
.set({
status: "approved",
approvedBy: ctx.userId,
approvedAt: new Date(),
})
.where(eq(invoices.id, record.id))
.returning();
return updated;
},
},
});Standalone Actions
Independent endpoint, no record required. Use standalone: true and optionally specify a custom path.
typescript
export default defineActions(sessions, {
chat: {
description: "Send a message to AI",
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", // Separate file
},
});Handler Files
For complex actions, separate the logic:
typescript
// handlers/chat.ts
import type { ActionExecutor } from '@quickback/types';
export const execute: ActionExecutor = async ({ db, ctx, input, services, c }) => {
// Complex business logic
return { success: true, data: result };
};Executor Parameters
typescript
interface ActionExecutorParams {
db: DrizzleDB; // Database instance
ctx: AppContext; // User context (userId, roles, etc.)
record?: TRecord; // The record (record-based only)
input: TInput; // Validated input from Zod schema
services: TServices; // Integrations (billing, notifications, etc.)
c: HonoContext; // Raw Hono context for advanced use
}Response Types
typescript
responseType: 'json' // Standard JSON (default)
responseType: 'stream' // Server-Sent Events
responseType: 'file' // File downloadRecord-Based vs Standalone
| Aspect | Record-Based | Standalone |
|---|---|---|
| Route | /:id/{actionName} | Custom path or /{actionName} |
| Default method | POST | POST |
| Record fetching | Yes (automatic) | No |
| Firewall applied | Yes | No |
record in executor | Available | undefined |
| Record preconditions | Supported via guard.record | Not applicable |
| Response types | JSON only | JSON, stream, file |