Skip to content

Latest commit

 

History

History
941 lines (801 loc) · 18.7 KB

File metadata and controls

941 lines (801 loc) · 18.7 KB
title HTTP API
description Standard REST mapping rules, CRUD operations, and request/response formats for ObjectStack

import { Radio, Code, Database, Lock, Zap, CheckCircle, AlertCircle } from 'lucide-react';

HTTP API

The HTTP API defines how ObjectStack maps data operations to RESTful HTTP endpoints. Every object you define automatically gets a complete set of CRUD (Create, Read, Update, Delete) operations with consistent request/response formats.

Core Principles

  1. Convention over Configuration: REST endpoints follow predictable patterns
  2. Consistency: Every object uses the same URL structure and response format
  3. Discoverability: API schema available via discovery endpoint
  4. Security First: Authentication and permissions enforced on every request
  5. Performance: Built-in caching, pagination, and field selection

API Discovery

Discovery Endpoint

Before making any API calls, clients should request the discovery endpoint to learn about available services:

Request:

GET /.well-known/objectstack HTTP/1.1
Host: api.acme.com

Alternative endpoint:

GET /api/v1 HTTP/1.1

Response:

{
  "name": "Acme CRM Production",
  "version": "2.1.0",
  "environment": "production",
  "routes": {
    "data": "/api/v1/data",
    "metadata": "/api/v1/meta",
    "auth": "/api/v1/auth",
    "actions": "/api/v1/actions",
    "storage": "/api/v1/storage",
    "graphql": "/api/v1/graphql",
    "realtime": "wss://api.acme.com/ws"
  },
  "features": {
    "graphql": true,
    "websocket": true,
    "search": true,
    "files": true,
    "batch": true,
    "webhooks": true
  },
  "limits": {
    "max_page_size": 100,
    "default_page_size": 25,
    "max_batch_size": 50,
    "rate_limit": 1000,
    "rate_window": "1m"
  },
  "locale": {
    "default": "en-US",
    "supported": ["en-US", "zh-CN", "es-ES", "fr-FR"],
    "timezone": "America/Los_Angeles"
  },
  "documentation": "https://docs.acme.com/api"
}

Why discovery matters:

  • Environment agnostic: Works across dev, staging, production without hardcoding URLs
  • Version tolerance: API routes can change without breaking clients
  • Feature detection: Clients enable/disable features based on server capabilities
  • Automatic configuration: SDKs auto-configure from discovery response

Standard Data API

All data operations use the base path from routes.data (default: /api/v1/data).

URL Structure

{base_path}/{object_name}/{record_id?}

Examples:

  • /api/v1/data/account - Account collection
  • /api/v1/data/account/acc_123 - Specific account
  • /api/v1/data/project_task - Project task collection (snake_case)

Authentication

All requests require authentication via one of these methods:

1. Bearer Token (JWT):

GET /api/v1/data/task
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

2. API Key:

GET /api/v1/data/task
X-API-Key: sk_live_abc123...

3. Session Cookie:

GET /api/v1/data/task
Cookie: session_id=xyz789...

Query Operations (List/Search)

Retrieve multiple records from an object.

Endpoint:

GET /{base_path}/{object_name}

Query Parameters:

Parameter Type Description Canonical Equivalent Example
select string Comma-separated field list fields id,name,status
filter JSON Filter criteria (see Filtering section) where {"status":"active"}
sort string Sort fields (prefix - for desc) orderBy -created_at,name
top number Max records to return limit 25
skip number Records to skip (offset) offset 50
expand string Related objects to embed expand assignee,comments
page number Page number (1-indexed) 2
per_page number Items per page (max from limits) 50

Transport → Protocol normalization: The HTTP dispatcher normalizes transport-level parameter names to Spec canonical (QueryAST) field names before forwarding to the broker layer: filterwhere, selectfields, sortorderBy, toplimit, skipoffset. The deprecated filters (plural) parameter is also accepted and normalized to where.

Example Request:

GET /api/v1/data/task?select=id,title,status&filter={"assignee_id":"user_123"}&sort=-created_at&page=1&per_page=25
Authorization: Bearer <token>

Success Response:

{
  "success": true,
  "data": [
    {
      "id": "task_456",
      "title": "Implement login page",
      "status": "in_progress",
      "created_at": "2024-01-15T10:30:00Z"
    },
    {
      "id": "task_789",
      "title": "Fix navigation bug",
      "status": "todo",
      "created_at": "2024-01-14T16:20:00Z"
    }
  ],
  "pagination": {
    "page": 1,
    "per_page": 25,
    "total": 47,
    "total_pages": 2,
    "has_next": true,
    "has_prev": false
  }
}

Filtering

Filters are passed as JSON in the filter query parameter.

Basic equality:

{ "status": "active" }
GET /api/data/account?filter={"status":"active"}

Multiple conditions (AND):

{
  "status": "active",
  "industry": "Technology"
}

Operators:

{
  "revenue": { "$gte": 100000 },
  "employees": { "$lte": 500 },
  "name": { "$contains": "Tech" },
  "created_at": { "$between": ["2024-01-01", "2024-12-31"] }
}

Supported operators:

  • $eq - Equals (default)
  • $ne - Not equals
  • $gt - Greater than
  • $gte - Greater than or equal
  • $lt - Less than
  • $lte - Less than or equal
  • $in - In array
  • $nin - Not in array
  • $contains - String contains
  • $startsWith - String starts with
  • $endsWith - String ends with
  • $between - Between two values
  • $null - Is null
  • $notNull - Is not null

OR conditions:

{
  "$or": [
    { "status": "urgent" },
    { "priority": "high" }
  ]
}

Complex nested filters:

{
  "$and": [
    { "status": "active" },
    {
      "$or": [
        { "industry": "Technology" },
        { "industry": "SaaS" }
      ]
    },
    { "revenue": { "$gte": 1000000 } }
  ]
}

Sorting

Sort by one or more fields using the sort parameter:

Single field ascending:

GET /api/data/account?sort=name

Single field descending (prefix with -):

GET /api/data/account?sort=-created_at

Multiple fields:

GET /api/data/account?sort=-priority,created_at

First sort by priority descending, then by created_at ascending.

Pagination

ObjectStack uses offset-based pagination:

Request page 2 with 50 items:

GET /api/data/account?page=2&per_page=50

Response includes pagination metadata:

{
  "success": true,
  "data": [...],
  "pagination": {
    "page": 2,
    "per_page": 50,
    "total": 247,
    "total_pages": 5,
    "has_next": true,
    "has_prev": true
  }
}

Pagination limits:

  • Default page size: 25 (from discovery limits.default_page_size)
  • Maximum page size: 100 (from discovery limits.max_page_size)
  • Requesting per_page > max_page_size returns HTTP 400

Field Selection

Request only the fields you need to reduce payload size:

Request:

GET /api/data/account?select=id,name,industry,revenue

Response:

{
  "success": true,
  "data": [
    {
      "id": "acc_123",
      "name": "Acme Corp",
      "industry": "Technology",
      "revenue": 5000000
    }
  ]
}

Benefits:

  • Reduced bandwidth (especially for mobile)
  • Faster response times
  • Lower server CPU usage

Note: System fields (id, created_at, updated_at) are always included even if not in select.

Including Related Objects

Embed related objects to avoid N+1 queries:

Request:

GET /api/data/task?include=assignee,project

Response:

{
  "success": true,
  "data": [
    {
      "id": "task_123",
      "title": "Implement API",
      "assignee_id": "user_456",
      "project_id": "proj_789",
      "assignee": {
        "id": "user_456",
        "name": "John Doe",
        "email": "john@acme.com"
      },
      "project": {
        "id": "proj_789",
        "name": "CRM Rebuild",
        "status": "active"
      }
    }
  ]
}

Multiple levels:

GET /api/data/task?include=assignee.department,project.owner

Limits:

  • Maximum include depth: 3 levels
  • Maximum included relations: 5 per request

Retrieve Single Record

Get a specific record by ID.

Endpoint:

GET /{base_path}/{object_name}/{record_id}

Example Request:

GET /api/v1/data/account/acc_123
Authorization: Bearer <token>

Success Response (HTTP 200):

{
  "success": true,
  "data": {
    "id": "acc_123",
    "name": "Acme Corporation",
    "industry": "Technology",
    "revenue": 5000000,
    "status": "active",
    "owner_id": "user_456",
    "created_at": "2024-01-10T14:30:00Z",
    "updated_at": "2024-01-15T09:20:00Z"
  }
}

Not Found (HTTP 404):

{
  "success": false,
  "error": {
    "code": "NOT_FOUND",
    "message": "Account with id 'acc_999' not found",
    "resource": "account",
    "resource_id": "acc_999"
  }
}

Create Record

Create a new record.

Endpoint:

POST /{base_path}/{object_name}

Request:

POST /api/v1/data/account
Authorization: Bearer <token>
Content-Type: application/json

{
  "name": "TechStart Inc",
  "industry": "SaaS",
  "revenue": 250000,
  "owner_id": "user_789"
}

Success Response (HTTP 201):

{
  "success": true,
  "data": {
    "id": "acc_124",
    "name": "TechStart Inc",
    "industry": "SaaS",
    "revenue": 250000,
    "status": "active",
    "owner_id": "user_789",
    "created_at": "2024-01-16T10:15:00Z",
    "updated_at": "2024-01-16T10:15:00Z"
  }
}

Validation Error (HTTP 400):

{
  "success": false,
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Validation failed",
    "fields": [
      {
        "field": "name",
        "message": "Name is required",
        "constraint": "required"
      },
      {
        "field": "industry",
        "message": "Must be one of: Technology, SaaS, Healthcare, Finance",
        "constraint": "enum",
        "value": "InvalidIndustry"
      }
    ]
  }
}

Update Record

Update an existing record (partial update).

Endpoint:

PATCH /{base_path}/{object_name}/{record_id}

Request:

PATCH /api/v1/data/account/acc_123
Authorization: Bearer <token>
Content-Type: application/json

{
  "revenue": 6000000,
  "status": "vip"
}

Success Response (HTTP 200):

{
  "success": true,
  "data": {
    "id": "acc_123",
    "name": "Acme Corporation",
    "industry": "Technology",
    "revenue": 6000000,
    "status": "vip",
    "owner_id": "user_456",
    "created_at": "2024-01-10T14:30:00Z",
    "updated_at": "2024-01-16T11:45:00Z"
  }
}

Note: Only fields included in the request body are updated. Other fields remain unchanged.

Read-only fields: Attempting to update read-only fields (e.g., id, created_at) returns HTTP 400:

{
  "success": false,
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Cannot update read-only fields",
    "fields": [
      {
        "field": "created_at",
        "message": "Field is read-only"
      }
    ]
  }
}

Delete Record

Delete a record by ID.

Endpoint:

DELETE /{base_path}/{object_name}/{record_id}

Request:

DELETE /api/v1/data/account/acc_123
Authorization: Bearer <token>

Success Response (HTTP 200):

{
  "success": true,
  "data": {
    "id": "acc_123",
    "deleted": true
  }
}

Soft Delete (if enabled): If object has enable.softDelete, record is marked deleted but not removed:

{
  "success": true,
  "data": {
    "id": "acc_123",
    "deleted": true,
    "deleted_at": "2024-01-16T12:00:00Z",
    "deleted_by": "user_456"
  }
}

Cascade Considerations: If object has related records and cascade delete is not enabled:

{
  "success": false,
  "error": {
    "code": "CONSTRAINT_VIOLATION",
    "message": "Cannot delete account with active opportunities",
    "constraint": "foreign_key",
    "related_object": "opportunity",
    "related_count": 5
  }
}

Batch Operations

Perform multiple operations in a single request.

Endpoint:

POST /{base_path}/_batch

Request:

POST /api/v1/data/_batch
Authorization: Bearer <token>
Content-Type: application/json

{
  "operations": [
    {
      "method": "POST",
      "path": "/account",
      "body": { "name": "Company A", "industry": "Tech" }
    },
    {
      "method": "PATCH",
      "path": "/account/acc_123",
      "body": { "status": "active" }
    },
    {
      "method": "DELETE",
      "path": "/account/acc_456"
    }
  ]
}

Response:

{
  "success": true,
  "results": [
    {
      "success": true,
      "status": 201,
      "data": { "id": "acc_789", "name": "Company A" }
    },
    {
      "success": true,
      "status": 200,
      "data": { "id": "acc_123", "status": "active" }
    },
    {
      "success": true,
      "status": 200,
      "data": { "id": "acc_456", "deleted": true }
    }
  ]
}

Limits:

  • Maximum batch size: 50 operations (from limits.max_batch_size)
  • Operations are executed sequentially
  • Failure of one operation doesn't stop others (non-transactional by default)

Transactional batch:

{
  "operations": [...],
  "atomic": true
}

If atomic: true, all operations execute in a transaction. If any fails, all are rolled back.

Metadata API

Retrieve object schemas and configuration.

Base path: From routes.metadata (default: /api/v1/meta)

List All Objects

Request:

GET /api/v1/meta/objects
Authorization: Bearer <token>

Response:

{
  "success": true,
  "data": [
    {
      "name": "account",
      "label": "Account",
      "plural_label": "Accounts",
      "description": "Business accounts and customers",
      "api_enabled": true,
      "searchable": true
    },
    {
      "name": "contact",
      "label": "Contact",
      "plural_label": "Contacts",
      "api_enabled": true,
      "searchable": true
    }
  ]
}

Get Object Schema

Request:

GET /api/v1/meta/objects/account
Authorization: Bearer <token>

Response:

{
  "success": true,
  "data": {
    "name": "account",
    "label": "Account",
    "plural_label": "Accounts",
    "fields": {
      "id": {
        "name": "id",
        "label": "ID",
        "type": "text",
        "readonly": true,
        "required": true
      },
      "name": {
        "name": "name",
        "label": "Account Name",
        "type": "text",
        "required": true,
        "maxLength": 255
      },
      "industry": {
        "name": "industry",
        "label": "Industry",
        "type": "select",
        "options": ["Technology", "SaaS", "Healthcare", "Finance"]
      },
      "revenue": {
        "name": "revenue",
        "label": "Annual Revenue",
        "type": "number",
        "format": "currency"
      }
    },
    "enable": {
      "trackHistory": true,
      "apiEnabled": true,
      "softDelete": true
    }
  }
}

Request Headers

Standard Headers

Required:

Authorization: Bearer <token>
Content-Type: application/json  # For POST/PATCH

Optional:

Accept-Language: en-US  # Preferred language
X-Request-ID: uuid  # Request tracking
X-API-Version: 2  # API version preference

CORS Headers

ObjectStack sends CORS headers automatically:

Access-Control-Allow-Origin: https://app.acme.com
Access-Control-Allow-Methods: GET, POST, PATCH, DELETE, OPTIONS
Access-Control-Allow-Headers: Authorization, Content-Type
Access-Control-Max-Age: 86400

Preflight request:

OPTIONS /api/v1/data/account
Origin: https://app.acme.com
Access-Control-Request-Method: POST

Preflight response:

HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.acme.com
Access-Control-Allow-Methods: POST
Access-Control-Max-Age: 86400

Caching

ObjectStack supports HTTP caching for GET requests:

Response with cache headers:

HTTP/1.1 200 OK
Cache-Control: private, max-age=60
ETag: "abc123def456"
Last-Modified: Wed, 15 Jan 2024 10:30:00 GMT

Conditional request:

GET /api/v1/data/account/acc_123
If-None-Match: "abc123def456"

Not modified response:

HTTP/1.1 304 Not Modified
ETag: "abc123def456"

Cache behavior:

  • GET requests: Cacheable with ETags
  • POST/PATCH/DELETE: Not cacheable
  • Cache duration: Configurable per object (default 60 seconds)

Rate Limiting

Requests include rate limit headers:

HTTP/1.1 200 OK
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 847
X-RateLimit-Reset: 1705324800

When limit exceeded:

HTTP/1.1 429 Too Many Requests
Retry-After: 45
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1705324800

{
  "success": false,
  "error": {
    "code": "RATE_LIMITED",
    "message": "Rate limit exceeded",
    "retry_after": 45,
    "limit": 1000,
    "window": "1m"
  }
}

See Error Handling for more details.

Best Practices

Use Field Selection

Bad: Fetch all fields when you only need a few

GET /api/data/account

Good: Request only needed fields

GET /api/data/account?select=id,name,status

Use Includes for Relations

Bad: N+1 queries

const tasks = await fetch('/api/data/task');
for (const task of tasks.data) {
  task.assignee = await fetch(`/api/data/user/${task.assignee_id}`);
}

Good: Single query with includes

const tasks = await fetch('/api/data/task?include=assignee');

Respect Rate Limits

Good: Check headers and implement backoff

const response = await fetch('/api/data/task');
const remaining = response.headers.get('X-RateLimit-Remaining');

if (remaining < 10) {
  console.warn('Approaching rate limit');
  await sleep(1000);
}

Handle Errors Gracefully

Good: Parse error structure

const response = await fetch('/api/data/task', { method: 'POST', body: data });
const result = await response.json();

if (!result.success) {
  if (result.error.code === 'VALIDATION_ERROR') {
    result.error.fields.forEach(field => {
      showFieldError(field.field, field.message);
    });
  }
}

Next Steps

} title="Real-Time Protocols" description="Learn WebSocket subscriptions and event streaming" href="/docs/transport/realtime" /> } title="Error Handling" description="Master error codes and debugging strategies" href="/docs/transport/error-handling" />