Skip to main content

APIs

TrailBase automatically generates type-safe REST APIs for your database tables and views, with built-in validation, access control, and TypeScript support.

API Overview

TrailBase provides three main API categories:

Record APIs

Auto-generated CRUD APIs for tables/views

Auth APIs

User authentication and session management

Admin APIs

System administration and configuration

Base URL Structure

http://localhost:4000/api/{category}/{version}/{endpoint}
  • category: records, auth, admin
  • version: v1 (API versioning for future changes)
  • endpoint: Specific operation path

Record APIs

Record API Module

Record APIs provide CRUD operations with automatic type validation and access control.

Endpoint Structure

All record API endpoints follow this pattern:
/api/records/v1/{api_name}/{operation}
From records/mod.rs:50-97, the router defines:
pub(crate) fn router(enable_transactions: bool) -> Router<AppState> {
  Router::new()
    // Get single record
    .route("/api/records/v1/{name}/{record}", get(read_record_handler))
    
    // Create record
    .route("/api/records/v1/{name}", post(create_record_handler))
    
    // Update record
    .route("/api/records/v1/{name}/{record}", patch(update_record_handler))
    
    // Delete record
    .route("/api/records/v1/{name}/{record}", delete(delete_record_handler))
    
    // List records
    .route("/api/records/v1/{name}", get(list_records_handler))
    
    // Get JSON schema
    .route("/api/records/v1/{name}/schema", get(json_schema_handler))
    
    // Subscribe to record changes (SSE)
    .route("/api/records/v1/{name}/subscribe/{record}", get(add_subscription_sse_handler))
    
    // File access
    .route("/api/records/v1/{name}/{record}/file/{column_name}", get(get_uploaded_file_from_record_handler))
}

Create Record

Endpoint: POST /api/records/v1/{api_name}
curl -X POST http://localhost:4000/api/records/v1/posts \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer <auth_token>" \
  -d '{
    "title": "My First Post",
    "content": "Hello, world!",
    "published": false
  }'
Response:
{
  "id": 123,
  "title": "My First Post",
  "content": "Hello, world!",
  "user_id": { "id": "base64_user_id" },
  "published": false,
  "created_at": 1704067200000
}
Implemented in records/create_record.rs. Fields are validated against the table’s JSON schema.

Read Record

Endpoint: GET /api/records/v1/{api_name}/{record_id}
curl http://localhost:4000/api/records/v1/posts/123
Query Parameters:
  • expand: Comma-separated foreign keys to expand
# With foreign key expansion
curl "http://localhost:4000/api/records/v1/posts/123?expand=user_id,category_id"
Response (with expansion):
{
  "id": 123,
  "title": "My First Post",
  "content": "Hello, world!",
  "user_id": {
    "id": "base64_user_id",
    "data": {
      "id": "base64_user_id",
      "email": "[email protected]",
      "verified": true
    }
  },
  "published": false,
  "created_at": 1704067200000
}

Read Implementation

Handles record fetching, foreign key expansion, and access control.

Update Record

Endpoint: PATCH /api/records/v1/{api_name}/{record_id}
curl -X PATCH http://localhost:4000/api/records/v1/posts/123 \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer <auth_token>" \
  -d '{
    "published": true
  }'
Features:
  • Partial updates (only send changed fields)
  • Validates against schema
  • Checks update access rules
  • Returns updated record
Implemented in records/update_record.rs with conflict resolution support.

Delete Record

Endpoint: DELETE /api/records/v1/{api_name}/{record_id}
curl -X DELETE http://localhost:4000/api/records/v1/posts/123 \
  -H "Authorization: Bearer <auth_token>"
Response: 204 No Content on success
Deletions are permanent and cannot be undone. Implement soft deletes if needed.

List Records

List Implementation

Provides filtering, sorting, pagination, and aggregation.
Endpoint: GET /api/records/v1/{api_name} Query Parameters:
ParameterTypeDescription
filterstringSQL WHERE clause expression
orderstringColumn to sort by
limitintegerMax records to return
offsetintegerNumber of records to skip
countbooleanInclude total count
expandstringForeign keys to expand
Example:
curl "http://localhost:4000/api/records/v1/posts?filter=published%3D1&order=created_at%20DESC&limit=10&count=true"
Response:
{
  "records": [
    {
      "id": 123,
      "title": "My First Post",
      "content": "Hello, world!",
      "published": true,
      "created_at": 1704067200000
    }
  ],
  "total_count": 42
}

Filtering

The filter parameter accepts SQL WHERE clause expressions:
# Single condition
?filter=published=1

# Multiple conditions
?filter=published=1 AND created_at > 1704067200000

# String matching
?filter=title LIKE '%tutorial%'

# NULL checks
?filter=deleted_at IS NULL

# IN clause
?filter=category_id IN (1, 2, 3)
Filter expressions are validated and sanitized to prevent SQL injection.

JSON Schema Endpoint

Endpoint: GET /api/records/v1/{api_name}/schema
curl http://localhost:4000/api/records/v1/posts/schema
Response:
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "title": "post",
  "type": "object",
  "properties": {
    "id": { "type": "integer" },
    "title": { "type": "string" },
    "content": { "type": ["string", "null"] },
    "published": { "type": "boolean" },
    "created_at": { "type": "integer" }
  },
  "required": ["title", "published", "created_at"]
}
Use this endpoint to generate TypeScript types or validate data client-side.

Real-time Subscriptions

Subscription System

Server-Sent Events (SSE) for real-time record updates.
Endpoint: GET /api/records/v1/{api_name}/subscribe/{record_id}
const eventSource = new EventSource(
  `http://localhost:4000/api/records/v1/posts/subscribe/123`,
  { withCredentials: true }
);

eventSource.onmessage = (event) => {
  const record = JSON.parse(event.data);
  console.log('Record updated:', record);
};

eventSource.onerror = (error) => {
  console.error('Subscription error:', error);
  eventSource.close();
};
How it works:
  1. Client establishes SSE connection
  2. Server creates SQLite trigger for the record
  3. On record update, trigger fires
  4. SubscriptionManager notifies active subscriptions
  5. Server sends updated record as SSE event
Subscriptions must be enabled in the Record API config: enable_subscriptions: true

File Upload & Download

Upload File

Files are uploaded as base64-encoded JSON:
curl -X POST http://localhost:4000/api/records/v1/users \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer <auth_token>" \
  -d '{
    "email": "[email protected]",
    "avatar": {
      "data": "<base64_encoded_image>",
      "mimeType": "image/jpeg",
      "name": "avatar.jpg"
    }
  }'

Download File

Endpoint: GET /api/records/v1/{api_name}/{record_id}/file/{column_name}
curl http://localhost:4000/api/records/v1/users/123/file/avatar \
  -o avatar.jpg
Files are served with proper MIME types and content-disposition headers.

Transactions

Transaction API

Batch multiple operations in a single atomic transaction.
Endpoint: POST /api/transaction/v1/execute
curl -X POST http://localhost:4000/api/transaction/v1/execute \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer <auth_token>" \
  -d '{
    "operations": [
      {
        "api_name": "posts",
        "operation": "create",
        "data": { "title": "Post 1", "content": "Content 1" }
      },
      {
        "api_name": "posts",
        "operation": "create",
        "data": { "title": "Post 2", "content": "Content 2" }
      }
    ]
  }'
Response:
{
  "results": [
    { "id": 123, "title": "Post 1", "content": "Content 1" },
    { "id": 124, "title": "Post 2", "content": "Content 2" }
  ]
}
Transactions must be explicitly enabled: server.enable_record_transactions: true

Auth APIs

Auth Router

Complete authentication API including registration, login, and OAuth.
Auth APIs are versioned at /api/auth/v1:

Register

POST /api/auth/v1/register
Content-Type: application/json

{
  "email": "[email protected]",
  "password": "secure_password123"
}

Login

POST /api/auth/v1/login
Content-Type: application/json

{
  "email": "[email protected]",
  "password": "secure_password123"
}
Response:
{
  "auth_token": "eyJhbGc...",
  "refresh_token": "...",
  "user_id": "base64_user_id",
  "expires_in": 3600
}

Refresh Token

POST /api/auth/v1/refresh
Cookie: refresh_token=<token>

Login Status

GET /api/auth/v1/status
Authorization: Bearer <auth_token>

Logout

GET /api/auth/v1/logout
Authorization: Bearer <auth_token>

OAuth

# Start OAuth flow
GET /api/auth/v1/oauth/login/{provider}?redirect_uri=<uri>&code_challenge=<pkce>

# OAuth callback (handled by provider)
GET /api/auth/v1/oauth/callback/{provider}?code=<code>&state=<state>

# Exchange code for tokens
POST /api/auth/v1/token
{
  "code": "<auth_code>",
  "code_verifier": "<pkce_verifier>"
}

Admin APIs

Admin APIs require authentication as an admin user and CSRF token validation.
Admin APIs are at /_/admin/api/ and include:
  • User management
  • Table operations (create, alter, drop)
  • Index management
  • Configuration updates
  • System logs
  • Job management

OpenAPI Documentation

TrailBase generates OpenAPI specs: Endpoint: GET /api/docs/openapi.json
// From lib.rs:117-133
#[derive(OpenApi)]
#[openapi(
  info(
    title = "TrailBase",
    description = "TrailBase APIs",
  ),
  nest(
    (path = "/api/auth/v1", api = crate::auth::AuthApi),
    (path = "/api/records/v1", api = crate::records::RecordOpenApi),
  ),
)]
pub struct Doc;
Use tools like Swagger UI or Redoc to view the interactive API documentation.

Type Safety

TypeScript Generation

Generate TypeScript types from JSON schemas:
# Fetch schema
curl http://localhost:4000/api/records/v1/posts/schema > post-schema.json

# Use json-schema-to-typescript
npx json-schema-to-typescript post-schema.json > post.types.ts
Generated types:
export interface Post {
  id: number;
  title: string;
  content?: string | null;
  user_id: string;
  published: boolean;
  created_at: number;
}

Client Libraries

Create type-safe API clients:
class PostsAPI {
  private baseUrl = 'http://localhost:4000/api/records/v1/posts';
  
  async create(data: Omit<Post, 'id' | 'created_at'>): Promise<Post> {
    const response = await fetch(this.baseUrl, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(data),
    });
    return response.json();
  }
  
  async get(id: number): Promise<Post> {
    const response = await fetch(`${this.baseUrl}/${id}`);
    return response.json();
  }
  
  async list(params?: {
    filter?: string;
    limit?: number;
    offset?: number;
  }): Promise<{ records: Post[]; total_count?: number }> {
    const query = new URLSearchParams(params).toString();
    const response = await fetch(`${this.baseUrl}?${query}`);
    return response.json();
  }
}

Error Handling

API errors follow standard HTTP status codes:
StatusMeaningExample
400Bad RequestInvalid JSON or schema validation failure
401UnauthorizedMissing or invalid auth token
403ForbiddenAccess denied by ACL or access rule
404Not FoundRecord or API doesn’t exist
409ConflictUnique constraint violation
429Too Many RequestsRate limit exceeded
500Internal Server ErrorServer-side error
Error Response Format:
{
  "error": "Validation failed",
  "details": "Field 'email' must be a valid email address"
}

Rate Limiting

API rate limits:
  • Auth Endpoints: 5 requests per 10 seconds (POST only)
  • Record APIs: No default limit (configure per-API if needed)
  • Key: Client IP address
Rate limit headers:
x-ratelimit-limit: 5
x-ratelimit-remaining: 4
x-ratelimit-reset: 1704067200

Best Practices

Use Pagination

Always set limit to avoid large responses

Validate Client-Side

Use JSON schemas for immediate feedback

Handle Errors

Check status codes and parse error messages

Cache Schemas

Schema endpoints can be cached client-side

Next Steps

Authentication

Learn how to authenticate API requests

Architecture

Understand how APIs are generated and served

Build docs developers (and LLMs) love