# CRUD Endpoints

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:`](/compiler/definitions/read).

> **Migration note**: `crud.list` and `crud.get` are rejected at compile time in DSL v2. List-level access moves to [`read.access`](/compiler/definitions/read), and per-field projections move to named views under [`read.views`](/compiler/definitions/read#views-under-read-views). See the [migration table](/compiler/definitions/read#migrating-from-crud-list-crud-get).

## Quick Reference

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

```bash
# 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](/compiler/definitions/read). Configuration (access, pagination, view projections) lives under `read:`. Filtering, sorting, and pagination query parameters work the same on both endpoints — see [Query Parameters](/compiler/targets/hono-api/using-the-api/query-params).

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

Calls a client makes:

```bash
GET /jobs                              # Collection — gated by read.access
GET /jobs/:id                          # Single — gated by read.access (with ID-probe defense)
GET /jobs/views/summary                # Named view projection
```

### 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](/compiler/definitions/read#access-ordering-on-id) 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'`](/compiler/definitions/firewall) 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](/compiler/definitions/schema#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

```json
{
  "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

```json
{
  "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/: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

```json
{
  "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:

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](/compiler/definitions/guards#putupsert-with-external-ids) 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)

```json
{
  "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

| 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

```json
{
  "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)

```json
{
  "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

| 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

```json
{
  "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

```json
{
  "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](/compiler/targets/hono-api/using-the-api/batch-operations#transactional-semantics).

```json
{
  "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

```json
{
  "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 write operations exist:

```typescript
// Auto-enabled - no configuration needed
create: { access: { roles: ['recruiter'] } },
update: { access: { roles: ['recruiter'] } }
// create.batch and update.batch automatically available

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

// Disable batch operations
create: {
  access: { roles: ['recruiter'] },
  batch: 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:

```json
{
  "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](/compiler/targets/hono-api/using-the-api/errors) for the complete reference of error codes by security layer.
