Quickback Docs
Quickback for Hono API

CRUD Endpoints

Auto-generated RESTful CRUD endpoints for each resource. Learn how to use create, update, delete, and upsert operations.

Quickback automatically generates RESTful CRUD endpoints for each resource you define. This page covers the write side (create, update, delete, upsert) and lists collection-read endpoints. Read-side configuration lives under read:.

Migration note: crud.list and crud.get are rejected at compile time in DSL v2. List-level access moves to read.access, and per-field projections move to named views under read.views. See the migration table.

Quick Reference

All endpoints require authentication. Include your session cookie or Bearer token:

# List records
curl http://localhost:8787/api/v1/jobs \
  -H "Authorization: Bearer <token>"

# Get a single record
curl http://localhost:8787/api/v1/jobs/job_123 \
  -H "Authorization: Bearer <token>"

# Create a record
curl -X POST http://localhost:8787/api/v1/jobs \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{"title": "Senior Engineer", "department": "Engineering", "status": "open"}'

# Update a record
curl -X PATCH http://localhost:8787/api/v1/jobs/job_123 \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{"title": "Staff Engineer"}'

# Delete a record
curl -X DELETE http://localhost:8787/api/v1/jobs/job_123 \
  -H "Authorization: Bearer <token>"

# List with filters, sorting, and pagination
curl "http://localhost:8787/api/v1/jobs?status=open&sort=createdAt:desc&limit=25&count=true" \
  -H "Authorization: Bearer <token>"

Endpoint Overview

For a resource named jobs, Quickback generates:

MethodEndpointDescription
GET/jobsList all records
GET/jobs/:idGet a single record
POST/jobsCreate a new record
POST/jobs/batchBatch create multiple records
PATCH/jobs/:idUpdate a record
PATCH/jobs/batchBatch update multiple records
DELETE/jobs/:idDelete a record
DELETE/jobs/batchBatch delete multiple records
PUT/jobs/:idUpsert a record (requires config)
PUT/jobs/batchBatch upsert multiple records (requires config)

List & Get Records

GET /{resource} and GET /{resource}/:id are part of the read pipeline. Configuration (access, pagination, view projections) lives under read:. Filtering, sorting, and pagination query parameters work the same on both endpoints — see Query Parameters.

read: {
  access: { roles: ['owner', 'hiring-manager', 'recruiter'] },
  pageSize: 25,
  maxPageSize: 100,
  views: {
    summary: { fields: ['id', 'title', 'status'], access: { roles: ['interviewer'] } },
  },
}

Calls a client makes:

GET /jobs                              # Collection — gated by read.access
GET /jobs/:id                          # Single — gated by read.access (with ID-probe defense)
GET /jobs?view=summary                 # View dispatch
GET /jobs/views/summary                # Explicit view path

ID-probe defense on /:id

Single-record reads run a role-only access check before the firewall query, so an unauthorized caller can't tell apart "row exists in another tenant" from "row doesn't exist." See Access ordering for details.

StatusDescription
401Not authenticated
403Authenticated but role doesn't satisfy read.access
404Authenticated and role passes, but firewall excluded the record (or it doesn't exist). Set firewallErrorMode: 'reveal' to return 403 instead.

FK Label Resolution

Foreign key columns are automatically enriched with _label fields containing the referenced table's display value. For example, departmentId gets a corresponding department_label field. See Display Column for details.

Create Record

POST /jobs
Content-Type: application/json

{
  "title": "Senior Engineer",
  "department": "Engineering",
  "status": "open",
  "salaryMin": 120000,
  "salaryMax": 180000
}

Creates a new record. Only fields listed in guards.createable are accepted.

Response

{
  "data": {
    "id": "job_456",
    "title": "Senior Engineer",
    "department": "Engineering",
    "status": "open",
    "salaryMin": 120000,
    "salaryMax": 180000,
    "createdAt": "2024-01-15T11:00:00Z",
    "createdBy": "user_789"
  }
}

Errors

StatusDescription
400Invalid field or missing required field
403User lacks permission to create records

Update Record

PATCH /jobs/:id
Content-Type: application/json

{
  "title": "Staff Engineer"
}

Updates an existing record. Only fields listed in guards.updatable are accepted.

Response

{
  "data": {
    "id": "job_123",
    "title": "Staff Engineer",
    "modifiedAt": "2024-01-15T12:00:00Z",
    "modifiedBy": "user_789"
  }
}

Errors

StatusDescription
400Invalid field or field not updatable
403User lacks permission to update this record
404Record not found

Delete Record

DELETE /jobs/:id

Deletes a record. Behavior depends on the delete.mode configuration.

Soft Delete (default)

Sets deletedAt and deletedBy fields. Record remains in database but is filtered from queries.

Cascade

When a parent is soft-deleted, every child table that holds an FK to it — both same-feature children and cross-feature children (e.g. applications referencing jobs from a different feature directory) — receives the same deletedAt / deletedBy stamp in the same request. The cascade UPDATE always AND-merges the child's own firewall (build<Child>FirewallConditions(ctx)), so a delete in one tenant can never reach a child row in another tenant.

If a child column declared .references(() => parent.id, { onDelete: '...' }), the cascade respects that intent — only cascade and set null participate. FKs without onDelete are cascaded for backwards-compat with the legacy shape.

Hard Delete

Permanently removes the record from the database.

Response

{
  "data": {
    "id": "job_123",
    "deleted": true
  }
}

Errors

StatusDescription
403User lacks permission to delete this record
404Record not found

Upsert Record (PUT)

PUT /jobs/:id
Content-Type: application/json

{
  "title": "Contract Recruiter",
  "department": "Talent Acquisition",
  "status": "open",
  "externalId": "ext-123"
}

Creates or updates a record by ID. Requires special configuration:

  1. generateId: false in database config
  2. guards: false in resource definition

Behavior

  • If record exists: Updates all provided fields
  • If record doesn't exist: Creates with the provided ID

Use Cases

  • Syncing data from external systems
  • Webhook handlers with external IDs
  • Idempotent operations (safe to retry)

See Guards documentation for setup details.

Batch Operations

Quickback provides batch endpoints for efficient bulk operations. Batch operations automatically inherit from their corresponding CRUD operations and maintain full security layer consistency.

Batch Create Records

POST /jobs/batch
Content-Type: application/json

{
  "records": [
    { "title": "Frontend Engineer", "department": "Engineering", "status": "open" },
    { "title": "Product Designer", "department": "Design", "status": "draft" },
    { "title": "Data Analyst", "department": "Analytics", "status": "open" }
  ],
  "options": {
    "failFast": false
  }
}

Creates multiple records in a single request. Each record follows the same validation rules as single create operations.

Request Body

FieldTypeDescription
recordsArrayArray of record objects to create
options.failFastBooleanIf true, stop on first error and roll back on tx-supporting providers (default: false)

Response (Partial Success - Default)

{
  "success": [
    { "id": "job_1", "title": "Frontend Engineer", "department": "Engineering", "status": "open" },
    { "id": "job_2", "title": "Product Designer", "department": "Design", "status": "draft" }
  ],
  "errors": [
    {
      "index": 2,
      "record": { "title": "Data Analyst", "department": "Analytics", "status": "open" },
      "error": {
        "error": "Field cannot be set during creation",
        "layer": "guards",
        "code": "GUARD_FIELD_NOT_CREATEABLE",
        "details": { "fields": ["status"] }
      }
    }
  ],
  "meta": {
    "total": 3,
    "succeeded": 2,
    "failed": 1,
    "failFast": false,
    "transactional": false
  }
}

HTTP Status Codes

  • 201 - All records created successfully
  • 207 - Partial success (some records failed)
  • 400 - failFast: true was set and one or more records failed

Batch Size Limit

Default: 100 records per request (configurable via maxBatchSize)

Batch Update Records

PATCH /jobs/batch
Content-Type: application/json

{
  "records": [
    { "id": "job_1", "title": "Senior Frontend Engineer" },
    { "id": "job_2", "department": "Product Design" },
    { "id": "job_3", "status": "closed" }
  ],
  "options": {
    "failFast": false
  }
}

Updates multiple records in a single request. All records must include an id field.

Request Body

FieldTypeDescription
recordsArrayArray of record objects with id and fields to update
options.failFastBooleanIf true, stop on first error and roll back on tx-supporting providers (default: false)

Response

{
  "success": [
    { "id": "job_1", "title": "Senior Frontend Engineer", "modifiedAt": "2024-01-15T14:00:00Z" },
    { "id": "job_2", "department": "Product Design", "modifiedAt": "2024-01-15T14:00:00Z" }
  ],
  "errors": [
    {
      "index": 2,
      "record": { "id": "job_3", "status": "closed" },
      "error": {
        "error": "Not found",
        "layer": "firewall",
        "code": "NOT_FOUND",
        "details": { "id": "job_3" }
      }
    }
  ],
  "meta": {
    "total": 3,
    "succeeded": 2,
    "failed": 1,
    "failFast": false,
    "transactional": false
  }
}

Features

  • Batch fetching: Single database query for all IDs (with firewall)
  • Per-record access: Access checks run with record context
  • Field validation: Guards apply to each record individually

Batch Delete Records

DELETE /jobs/batch
Content-Type: application/json

{
  "ids": ["job_1", "job_2", "job_3"],
  "options": {
    "failFast": false
  }
}

Deletes multiple records in a single request. Supports both soft and hard delete modes.

Request Body

FieldTypeDescription
idsArrayArray of record IDs to delete
options.failFastBooleanIf true, stop on first error and roll back on tx-supporting providers (default: false)

Response (Soft Delete)

{
  "success": [
    { "id": "job_1", "deletedAt": "2024-01-15T15:00:00Z", "deletedBy": "user_789" },
    { "id": "job_2", "deletedAt": "2024-01-15T15:00:00Z", "deletedBy": "user_789" }
  ],
  "errors": [
    {
      "index": 2,
      "id": "job_3",
      "error": {
        "error": "Not found",
        "layer": "firewall",
        "code": "NOT_FOUND",
        "details": { "id": "job_3" }
      }
    }
  ],
  "meta": {
    "total": 3,
    "succeeded": 2,
    "failed": 1,
    "failFast": false,
    "transactional": false
  }
}

Delete Modes

  • Soft delete (default): Sets deletedAt, deletedBy, modifiedAt, modifiedBy fields
  • Hard delete: Permanently removes records from database

Batch Upsert Records

PUT /jobs/batch
Content-Type: application/json

{
  "records": [
    { "id": "job_1", "title": "Updated Frontend Engineer", "department": "Engineering" },
    { "id": "new_job", "title": "New Backend Engineer", "department": "Engineering", "status": "draft" }
  ],
  "options": {
    "failFast": false
  }
}

Creates or updates multiple records in a single request. Creates if ID doesn't exist, updates if it does.

Strict Requirements (same as single PUT):

  1. generateId: false in database config (user provides IDs)
  2. guards: false in resource definition (no field restrictions)
  3. All records must include an id field

Note: System-managed fields (createdAt, createdBy, modifiedAt, modifiedBy, deletedAt, deletedBy) are always protected and will be rejected if included in the request, regardless of guards configuration.

Request Body

FieldTypeDescription
recordsArrayArray of record objects with id and all fields
options.failFastBooleanIf true, stop on first error and roll back on tx-supporting providers (default: false)

Response

{
  "success": [
    { "id": "job_1", "title": "Updated Frontend Engineer", "department": "Engineering", "modifiedAt": "2024-01-15T16:00:00Z" },
    { "id": "new_job", "title": "New Backend Engineer", "department": "Engineering", "status": "draft", "createdAt": "2024-01-15T16:00:00Z" }
  ],
  "errors": [],
  "meta": {
    "total": 2,
    "succeeded": 2,
    "failed": 0,
    "atomic": false
  }
}

How It Works

  1. Batch existence check with firewall
  2. Split records into CREATE and UPDATE batches
  3. Validate new records with validateCreate()
  4. Validate existing records with validateUpdate()
  5. Check CREATE access for new records
  6. Check UPDATE access for existing records (per-record)
  7. Execute bulk insert and individual updates
  8. Return combined results

Batch Operation Features

Partial Success Mode (Default)

By default, batch operations use partial success mode:

  • All records are processed independently
  • Failed records go into errors array with detailed error information
  • Successful records go into success array
  • HTTP status 207 Multi-Status if any errors, 201/200 if all success
{
  "success": [ /* succeeded records */ ],
  "errors": [
    {
      "index": 2,
      "record": { /* original input */ },
      "error": {
        "error": "Human-readable message",
        "layer": "guards",
        "code": "GUARD_FIELD_NOT_CREATEABLE",
        "details": { "fields": ["status"] },
        "hint": "These fields are set automatically or must be omitted"
      }
    }
  ],
  "meta": {
    "total": 10,
    "succeeded": 8,
    "failed": 2,
    "atomic": false
  }
}

Fail-Fast Mode (Opt-in)

Set options.failFast: true to stop on the first error. On providers that support transactions, the loop is wrapped in db.transaction(...) so prior writes roll back. The response's meta.transactional field tells you whether you got real rollback or just early-stop semantics — see Batch Operations: Transactional semantics.

{
  "records": [ /* ... */ ],
  "options": {
    "failFast": true
  }
}

On the first failure under failFast: true:

  • Processing stops; no further records are attempted
  • HTTP status 400 Bad Request
  • A single error is returned
{
  "error": "Batch failed at index 2",
  "layer": "validation",
  "code": "BATCH_FAILFAST_STOPPED",
  "details": {
    "failedAt": 2,
    "reason": { /* the actual error */ }
  }
}

Human-Readable Errors

All batch operation errors include:

  • Layer identification: Which security layer rejected the request
  • Error code: Machine-readable code for programmatic handling
  • Clear message: Human-readable explanation
  • Details: Contextual information (fields, IDs, reasons)
  • Helpful hints: Actionable guidance for resolution

Performance Optimizations

  • Batch size limits: Default 100 records (prevents memory exhaustion)
  • Single firewall query: WHERE id IN (...) instead of N queries
  • Bulk operations: Single INSERT for multiple records (CREATE, UPSERT)
  • O(1) lookups: Map-based record lookup instead of Array.find()

Configuration

Batch operations are auto-enabled when corresponding CRUD operations exist:

// Auto-enabled - no configuration needed
crud: {
  create: { access: { roles: ['recruiter'] } },
  update: { access: { roles: ['recruiter'] } }
  // batchCreate and batchUpdate automatically available
}

// Customize batch operations
crud: {
  create: { access: { roles: ['recruiter'] } },
  batchCreate: {
    access: { roles: ['hiring-manager'] },  // Different access rules
    maxBatchSize: 50,               // Lower limit
    allowFailFast: false            // Disable failFast mode
  }
}

// Disable batch operations
crud: {
  create: { access: { roles: ['recruiter'] } },
  batchCreate: false  // Explicitly disable
}

Security Layer Application

Batch operations maintain full security layer consistency:

  1. Firewall: Auto-apply ownership fields, batch fetch with isolation
  2. Access: Operation-level for CREATE, per-record for UPDATE/DELETE
  3. Guards: Per-record field validation (same rules as single operations)
  4. Masking: Applied to success array (respects user permissions)
  5. Audit: Single timestamp for entire batch for consistency

Authentication

All endpoints require authentication. Include your auth token in the request header:

Authorization: Bearer <your-token>

The user's context (userId, roles, organizationId) is extracted from the token and used to:

  1. Apply firewall filters (data isolation)
  2. Check access permissions
  3. Set audit fields (createdBy, modifiedBy)

Error Responses

All errors use a flat structure with contextual fields:

{
  "error": "Insufficient permissions",
  "layer": "access",
  "code": "ACCESS_ROLE_REQUIRED",
  "details": {
    "required": ["hiring-manager"],
    "current": ["interviewer"]
  },
  "hint": "Contact an administrator to grant necessary permissions"
}

See Errors for the complete reference of error codes by security layer.

On this page