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.listandcrud.getare rejected at compile time in DSL v2. List-level access moves toread.access, and per-field projections move to named views underread.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:
| Method | Endpoint | Description |
|---|---|---|
GET | /jobs | List all records |
GET | /jobs/:id | Get a single record |
POST | /jobs | Create a new record |
POST | /jobs/batch | Batch create multiple records |
PATCH | /jobs/:id | Update a record |
PATCH | /jobs/batch | Batch update multiple records |
DELETE | /jobs/:id | Delete a record |
DELETE | /jobs/batch | Batch delete multiple records |
PUT | /jobs/:id | Upsert a record (requires config) |
PUT | /jobs/batch | Batch 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 pathID-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.
| Status | Description |
|---|---|
401 | Not authenticated |
403 | Authenticated but role doesn't satisfy read.access |
404 | Authenticated 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
| Status | Description |
|---|---|
400 | Invalid field or missing required field |
403 | User 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
| Status | Description |
|---|---|
400 | Invalid field or field not updatable |
403 | User lacks permission to update this record |
404 | Record not found |
Delete Record
DELETE /jobs/:idDeletes 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
| Status | Description |
|---|---|
403 | User lacks permission to delete this record |
404 | Record 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:
generateId: falsein database configguards: falsein 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
| Field | Type | Description |
|---|---|---|
records | Array | Array of record objects to create |
options.failFast | Boolean | If 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 successfully207- Partial success (some records failed)400-failFast: truewas 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
| Field | Type | Description |
|---|---|---|
records | Array | Array of record objects with id and fields to update |
options.failFast | Boolean | If 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
| Field | Type | Description |
|---|---|---|
ids | Array | Array of record IDs to delete |
options.failFast | Boolean | If 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,modifiedByfields - 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):
generateId: falsein database config (user provides IDs)guards: falsein resource definition (no field restrictions)- All records must include an
idfield
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
| Field | Type | Description |
|---|---|---|
records | Array | Array of record objects with id and all fields |
options.failFast | Boolean | If 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
- Batch existence check with firewall
- Split records into CREATE and UPDATE batches
- Validate new records with
validateCreate() - Validate existing records with
validateUpdate() - Check CREATE access for new records
- Check UPDATE access for existing records (per-record)
- Execute bulk insert and individual updates
- 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
errorsarray with detailed error information - Successful records go into
successarray - HTTP status
207 Multi-Statusif any errors,201/200if 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:
- Firewall: Auto-apply ownership fields, batch fetch with isolation
- Access: Operation-level for CREATE, per-record for UPDATE/DELETE
- Guards: Per-record field validation (same rules as single operations)
- Masking: Applied to success array (respects user permissions)
- 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:
- Apply firewall filters (data isolation)
- Check access permissions
- 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.