Skip to main content

Overview

The REST API provides a simple HTTP endpoint to retrieve all accumulated database changes. This is useful for initial page loads, polling-based implementations, or when WebSocket connections are not feasible.

Endpoints

GET /api/changes

Retrieve all database changes that have been captured since the server started.
curl http://localhost:3000/api/changes
None
void
This endpoint accepts no query parameters. It returns all accumulated changes.

Response

Response
Change[]
Array of all database change objects in chronological order.
type Change = {
  operation: string;
  table: string;
  [key: string]: any;
};

Response Fields

Request Examples

Using cURL

curl http://localhost:3000/api/changes

Using fetch (JavaScript)

fetch("http://localhost:3000/api/changes")
  .then(response => response.json())
  .then(changes => {
    console.log("All changes:", changes);
  })
  .catch(error => {
    console.error("Error fetching changes:", error);
  });

Using axios

import axios from 'axios';

const response = await axios.get('http://localhost:3000/api/changes');
const changes = response.data;

Using TypeScript

interface Change {
  operation: string;
  table: string;
  [key: string]: any;
}

async function fetchChanges(): Promise<Change[]> {
  const response = await fetch('http://localhost:3000/api/changes');
  
  if (!response.ok) {
    throw new Error(`HTTP error! status: ${response.status}`);
  }
  
  return await response.json();
}

// Usage
const changes = await fetchChanges();
console.log(`Retrieved ${changes.length} changes`);

Response Examples

Empty Response

When no database changes have occurred:
[]

Single Change

[
  {
    "operation": "INSERT",
    "table": "public.users",
    "id": 1,
    "name": "John Doe",
    "email": "[email protected]",
    "created_at": "2026-03-03T10:30:00Z"
  }
]

Multiple Changes

[
  {
    "operation": "INSERT",
    "table": "public.users",
    "id": 1,
    "name": "John Doe",
    "email": "[email protected]",
    "created_at": "2026-03-03T10:30:00Z"
  },
  {
    "operation": "UPDATE",
    "table": "public.products",
    "id": 42,
    "name": "Widget Pro",
    "price": 39.99,
    "updated_at": "2026-03-03T11:15:00Z"
  },
  {
    "operation": "DELETE",
    "table": "public.old_records",
    "id": 99,
    "deleted_at": "2026-03-03T12:00:00Z"
  }
]

Response with Various Data Types

[
  {
    "operation": "INSERT",
    "table": "public.orders",
    "id": 123,
    "user_id": 1,
    "amount": 99.99,
    "status": "pending",
    "items": 5,
    "is_paid": false,
    "created_at": "2026-03-03T12:30:00Z",
    "metadata": null
  }
]

Response Headers

HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 1234
Content-Type
string
Always application/json for successful responses
Content-Length
number
Size of the response body in bytes

Status Codes

200
OK
Successful request. Returns array of changes (may be empty).
500
Internal Server Error
Server error occurred while processing the request.

Server Implementation

The endpoint is implemented in the Bun server:
// Accumulative array for all changes
const allChanges: Array<Record<string, any>> = [];

const server = serve({
  async fetch(req, server) {
    const url = new URL(req.url);

    // API endpoint
    if (url.pathname === "/api/changes") {
      return Response.json(allChanges);
    }

    return undefined;
  },
});
The allChanges array accumulates changes in memory. It persists for the lifetime of the server process but is reset when the server restarts.

Data Persistence

Changes are stored in-memory only. When the server restarts, the changes array is reset to empty. For persistent storage, you would need to implement a database or file-based storage solution.

Use Cases

Initial Data Load

Fetch all changes when your application first loads before establishing a WebSocket connection.

Polling

Periodically poll this endpoint if WebSocket connections are not available in your environment.

Data Export

Export changes for analysis, logging, or integration with other systems.

Debugging

Quickly check what changes have been captured during development and testing.

Polling Implementation

If you need to use polling instead of WebSockets:
let previousLength = 0;

async function pollChanges() {
  try {
    const response = await fetch('http://localhost:3000/api/changes');
    const changes = await response.json();
    
    // Check if new changes arrived
    if (changes.length > previousLength) {
      const newChanges = changes.slice(previousLength);
      console.log('New changes:', newChanges);
      previousLength = changes.length;
    }
  } catch (error) {
    console.error('Polling error:', error);
  }
}

// Poll every 5 seconds
setInterval(pollChanges, 5000);
While polling works, WebSocket connections provide better performance and lower latency for real-time updates. Use the REST API primarily for initial loads or environments where WebSockets are restricted.

Combining REST and WebSocket

Best practice is to use both APIs together:
async function initializeApp() {
  // 1. Load initial data via REST
  const response = await fetch('http://localhost:3000/api/changes');
  const initialChanges = await response.json();
  setChanges(initialChanges);
  
  // 2. Connect WebSocket for real-time updates
  const ws = new WebSocket('ws://localhost:3000/ws');
  
  ws.onmessage = (event) => {
    const message = JSON.parse(event.data);
    
    // Skip initial message since we already loaded via REST
    if (message.type === 'change') {
      setChanges(prev => [...prev, ...message.data]);
    }
  };
}

Performance Considerations

The in-memory array grows with each database change. Monitor memory usage if expecting high-volume changes. Consider implementing pagination or size limits for production use.
Response size increases linearly with the number of changes. For large datasets, consider:
  • Implementing pagination with query parameters
  • Adding filters by table or operation type
  • Limiting the time range of returned changes
This endpoint returns dynamic data that changes frequently. Avoid aggressive caching. If caching, use very short TTL values (seconds, not minutes).

Error Handling

async function fetchChangesWithErrorHandling() {
  try {
    const response = await fetch('http://localhost:3000/api/changes');
    
    if (!response.ok) {
      throw new Error(`HTTP ${response.status}: ${response.statusText}`);
    }
    
    const changes = await response.json();
    return changes;
    
  } catch (error) {
    if (error instanceof TypeError) {
      console.error('Network error:', error);
      // Handle network issues
    } else {
      console.error('Unexpected error:', error);
    }
    
    return [];
  }
}

CORS Configuration

The server runs on localhost:3000 by default. If your frontend runs on a different port, you may need to configure CORS headers.
For cross-origin requests, modify the server response:
if (url.pathname === "/api/changes") {
  return new Response(JSON.stringify(allChanges), {
    headers: {
      'Content-Type': 'application/json',
      'Access-Control-Allow-Origin': '*',
      'Access-Control-Allow-Methods': 'GET, OPTIONS',
    },
  });
}

Testing the Endpoint

Check Server Status

# Verify the endpoint is accessible
curl -I http://localhost:3000/api/changes

Format JSON Output

# Pretty-print JSON response
curl http://localhost:3000/api/changes | json_pp

# Or with jq
curl http://localhost:3000/api/changes | jq '.'

Count Changes

# Count number of changes
curl -s http://localhost:3000/api/changes | jq 'length'

Filter by Operation

# Get only INSERT operations
curl -s http://localhost:3000/api/changes | jq '[.[] | select(.operation == "INSERT")]'

Build docs developers (and LLMs) love