Documentation
Everything you need to integrate the API your database deserves.
Quickstart
- Sign in to the SWAIN platform and connect your database.
- Select tables and configure auth & security.
- Generate and deploy your backend.
Basic API Call Examples
curl -H "Authorization: Bearer <JWT>" \
"https://api.your-domain.com/api/user?page=1&pageSize=25&sort=name:asc"
const token = 'YOUR_JWT_TOKEN';
const baseUrl = 'https://api.your-domain.com';
async function getUsers() {
const response = await fetch(`${baseUrl}/api/user?page=1&pageSize=25&sort=name:asc`, {
headers: {
'Authorization': `Bearer ${token}`
}
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
return data;
}
// Usage
getUsers()
.then(users => console.log('Users:', users))
.catch(error => console.error('Error:', error));
interface User {
id: number;
name: string;
email: string;
status: 'active' | 'inactive';
created_at: string;
}
interface ApiResponse<T> {
data: T[];
total: number;
page: number;
page_size: number;
total_pages: number;
}
class SwainAPI {
private baseUrl = 'https://api.your-domain.com';
private token: string;
constructor(token: string) {
this.token = token;
}
async getUsers(page = 1, pageSize = 25): Promise<ApiResponse<User>> {
const response = await fetch(
`${this.baseUrl}/api/user?page=${page}&pageSize=${pageSize}&sort=name:asc`,
{
headers: {
'Authorization': `Bearer ${this.token}`,
'Content-Type': 'application/json'
}
}
);
if (!response.ok) {
throw new Error(`API Error: ${response.status}`);
}
return response.json();
}
}
// Usage
const api = new SwainAPI('YOUR_JWT_TOKEN');
api.getUsers().then(response => {
console.log(`Found ${response.data.length} users`);
});
import requests
from typing import Dict, List, Optional
class SwainAPI:
def __init__(self, base_url: str, token: str):
self.base_url = base_url
self.headers = {
'Authorization': f'Bearer {token}',
'Content-Type': 'application/json'
}
def get_users(self, page: int = 1, page_size: int = 25,
sort: str = "name:asc") -> Dict:
"""Fetch users with pagination."""
params = {
'page': page,
'pageSize': page_size,
'sort': sort
}
response = requests.get(
f"{self.base_url}/api/user",
headers=self.headers,
params=params
)
response.raise_for_status()
return response.json()
def filter_users(self, expressions: List[Dict],
projections: Optional[List[str]] = None) -> Dict:
"""Filter users with complex queries."""
filter_data = {
'expressions': expressions
}
if projections:
filter_data['projections'] = projections
response = requests.post(
f"{self.base_url}/api/user/filter",
headers=self.headers,
json=filter_data
)
response.raise_for_status()
return response.json()
# Usage
api = SwainAPI('https://api.your-domain.com', 'YOUR_JWT_TOKEN')
# Simple get
users = api.get_users(page=1, page_size=25)
print(f"Found {len(users['data'])} users")
# Complex filter
active_users = api.filter_users(
expressions=[
{'field': 'status', 'operator': 'eq', 'value': 'active'}
],
projections=['id', 'name', 'email']
)
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
)
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
Status string `json:"status"`
CreatedAt string `json:"created_at"`
}
type ApiResponse struct {
Data []User `json:"data"`
Total int64 `json:"total"`
Page int `json:"page"`
PageSize int `json:"page_size"`
TotalPages int64 `json:"total_pages"`
}
type SwainClient struct {
BaseURL string
Token string
Client *http.Client
}
func NewSwainClient(baseURL, token string) *SwainClient {
return &SwainClient{
BaseURL: baseURL,
Token: token,
Client: &http.Client{},
}
}
func (c *SwainClient) GetUsers(page, pageSize int) (*ApiResponse, error) {
url := fmt.Sprintf("%s/api/user?page=%d&pageSize=%d&sort=name:asc",
c.BaseURL, page, pageSize)
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+c.Token)
req.Header.Set("Content-Type", "application/json")
resp, err := c.Client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
var apiResponse ApiResponse
if err := json.Unmarshal(body, &apiResponse); err != nil {
return nil, err
}
return &apiResponse, nil
}
// Usage
func main() {
client := NewSwainClient("https://api.your-domain.com", "YOUR_JWT_TOKEN")
response, err := client.GetUsers(1, 25)
if err != nil {
panic(err)
}
fmt.Printf("Found %d users\n", len(response.Data))
}
require 'httparty'
require 'json'
class SwainAPI
include HTTParty
def initialize(base_url, token)
@base_url = base_url
@headers = {
'Authorization' => "Bearer #{token}",
'Content-Type' => 'application/json'
}
end
def get_users(page: 1, page_size: 25, sort: 'name:asc')
query = {
page: page,
pageSize: page_size,
sort: sort
}
response = self.class.get(
"#{@base_url}/api/user",
headers: @headers,
query: query
)
handle_response(response)
end
def filter_users(expressions:, projections: nil)
body = { expressions: expressions }
body[:projections] = projections if projections
response = self.class.post(
"#{@base_url}/api/user/filter",
headers: @headers,
body: body.to_json
)
handle_response(response)
end
private
def handle_response(response)
case response.code
when 200..299
response.parsed_response
else
raise "API Error: #{response.code} - #{response.message}"
end
end
end
# Usage
api = SwainAPI.new('https://api.your-domain.com', 'YOUR_JWT_TOKEN')
# Simple get request
users = api.get_users(page: 1, page_size: 25)
puts "Found #{users['data'].length} users"
# Filter active users
active_users = api.filter_users(
expressions: [
{ field: 'status', operator: 'eq', value: 'active' }
],
projections: ['id', 'name', 'email']
)
<?php
require 'vendor/autoload.php';
use GuzzleHttp\Client;
use GuzzleHttp\Exception\RequestException;
class SwainAPI {
private $client;
private $baseUrl;
private $token;
public function __construct(string $baseUrl, string $token) {
$this->baseUrl = $baseUrl;
$this->token = $token;
$this->client = new Client([
'base_uri' => $baseUrl,
'headers' => [
'Authorization' => 'Bearer ' . $token,
'Content-Type' => 'application/json'
]
]);
}
public function getUsers(int $page = 1, int $pageSize = 25,
string $sort = 'name:asc'): array {
try {
$response = $this->client->get('/api/user', [
'query' => [
'page' => $page,
'pageSize' => $pageSize,
'sort' => $sort
]
]);
return json_decode($response->getBody(), true);
} catch (RequestException $e) {
throw new Exception('API Error: ' . $e->getMessage());
}
}
public function filterUsers(array $expressions,
?array $projections = null): array {
$body = ['expressions' => $expressions];
if ($projections) {
$body['projections'] = $projections;
}
try {
$response = $this->client->post('/api/user/filter', [
'json' => $body
]);
return json_decode($response->getBody(), true);
} catch (RequestException $e) {
throw new Exception('API Error: ' . $e->getMessage());
}
}
}
// Usage
$api = new SwainAPI('https://api.your-domain.com', 'YOUR_JWT_TOKEN');
// Get users
$users = $api->getUsers(1, 25);
echo "Found " . count($users['data']) . " users\n";
// Filter active users
$activeUsers = $api->filterUsers(
[
['field' => 'status', 'operator' => 'eq', 'value' => 'active']
],
['id', 'name', 'email']
);
using System;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
using System.Collections.Generic;
public class User
{
public int Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
public string Status { get; set; }
[JsonPropertyName("created_at")] public DateTime CreatedAt { get; set; }
}
public class ApiResponse<T>
{
[JsonPropertyName("data")] public List<T> Data { get; set; }
[JsonPropertyName("total")] public long Total { get; set; }
[JsonPropertyName("page")] public int Page { get; set; }
[JsonPropertyName("page_size")] public int PageSize { get; set; }
[JsonPropertyName("total_pages")] public long TotalPages { get; set; }
}
public class SwainClient
{
private readonly HttpClient _httpClient;
private readonly string _baseUrl;
public SwainClient(string baseUrl, string token)
{
_baseUrl = baseUrl;
_httpClient = new HttpClient();
_httpClient.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", token);
_httpClient.DefaultRequestHeaders.Accept.Add(
new MediaTypeWithQualityHeaderValue("application/json"));
}
public async Task<ApiResponse<User>> GetUsersAsync(
int page = 1, int pageSize = 25, string sort = "name:asc")
{
var url = $"{_baseUrl}/api/user?page={page}&pageSize={pageSize}&sort={sort}";
var response = await _httpClient.GetAsync(url);
response.EnsureSuccessStatusCode();
var json = await response.Content.ReadAsStringAsync();
return JsonSerializer.Deserialize<ApiResponse<User>>(json);
}
public async Task<ApiResponse<User>> FilterUsersAsync(
object filterExpression)
{
var url = $"{_baseUrl}/api/user/filter";
var json = JsonSerializer.Serialize(filterExpression);
var content = new StringContent(json, Encoding.UTF8, "application/json");
var response = await _httpClient.PostAsync(url, content);
response.EnsureSuccessStatusCode();
var responseJson = await response.Content.ReadAsStringAsync();
return JsonSerializer.Deserialize<ApiResponse<User>>(responseJson);
}
}
// Usage
class Program
{
static async Task Main()
{
var client = new SwainClient(
"https://api.your-domain.com",
"YOUR_JWT_TOKEN"
);
// Get users
var users = await client.GetUsersAsync(1, 25);
Console.WriteLine($"Found {users.Data.Count} users");
// Filter active users
var filter = new
{
expressions = new[]
{
new { field = "status", @operator = "eq", value = "active" }
},
projections = new[] { "id", "name", "email" }
};
var activeUsers = await client.FilterUsersAsync(filter);
Console.WriteLine($"Found {activeUsers.Data.Count} active users");
}
}
Complex Filter Example
{
"expressions": [
{ "field": "status", "operator": "eq", "value": "active" },
{
"relationship": "customer",
"expressions": [
{ "field": "region", "operator": "in", "value": ["EU","US"] }
],
"include": true
}
],
"projections": ["id", "name", "status", "created_at"]
}
Endpoints
| Method | Path | Description |
|---|---|---|
| GET | /api/{model} | List with pagination & sorting |
| GET | /api/{model}/{id} | Fetch a single record |
| POST | /api/{model} | Create a new record |
| PUT | /api/{model}/{id} | Update a record |
| DELETE | /api/{model}/{id} | Delete a record |
| POST | /api/{model}/filter | Advanced JSON filtering & relation loading |
| PUT | /api/{model}/filter | Bulk update by filter |
| DELETE | /api/{model}/filter | Bulk delete by filter |
| POST | /api/{model}/count | Count entities by filter |
| GET | /api/{model}/schema | Get model schema information |
| GET | /api/models | List all available models |
| GET | /api/dynamic_swagger | Get OpenAPI/Swagger documentation |
Note: Runtime routes take precedence over inline code annotations. For example, counting uses /api/{model}/count at runtime and in Swagger UI (/swagger/), even if some annotations reference /{model}/filter/count.
Filtering
Send JSON to /api/{model}/filter or include JSON body in GET /api/{model} requests. The filter uses three expression types: FieldExpression for simple comparisons, LogicalExpression for combining with AND/OR/NOT, and RelationshipExpression for filtering on related entities. Multiple expressions at the root level are combined with AND by default. Pagination parameters (page, pageSize, sort) are passed as query parameters even for POST requests.
Operators Reference
| Operator | Description | Example | Notes |
|---|---|---|---|
eq | Equals | {"field":"status","operator":"eq","value":"active"} | |
neq | Not equals | {"field":"age","operator":"neq","value":30} | |
gt | Greater than | {"field":"age","operator":"gt","value":30} | |
gte | Greater or equal | {"field":"age","operator":"gte","value":18} | |
lt | Less than | {"field":"price","operator":"lt","value":1000} | |
lte | Less or equal | {"field":"age","operator":"lte","value":65} | |
like | Pattern match | {"field":"name","operator":"like","value":"John%"} | Use % for wildcards |
not_like | Not like | {"field":"email","operator":"not_like","value":"%spam%"} | Use % for wildcards |
contains | Substring match | {"field":"description","operator":"contains","value":"important"} | Auto-wraps with % |
in | In array | {"field":"status","operator":"in","value":["active","pending"]} | Value must be array |
not_in | Not in array | {"field":"id","operator":"not_in","value":[1,2,3]} | Value must be array |
is_null | Is NULL | {"field":"deleted_at","operator":"is_null"} | No value or null value |
is_not_null | Is not NULL | {"field":"deleted_at","operator":"is_not_null"} | No value or null value |
# Complex filtering with POST
curl -X POST "https://api.your-domain.com/api/user/filter?page=1&pageSize=50" \
-H "Authorization: Bearer <JWT>" \
-H "Content-Type: application/json" \
-d '{
"expressions": [
{"field": "status", "operator": "eq", "value": "active"}
]
}'
# GET requests can also accept JSON body for filtering
curl -X GET "https://api.your-domain.com/api/user?page=1&pageSize=50" \
-H "Authorization: Bearer <JWT>" \
-H "Content-Type: application/json" \
-d '{
"expressions": [
{"field": "active", "operator": "eq", "value": true}
]
}'
import requests
token = "YOUR_JWT"
base = "https://api.your-domain.com"
q = {
"expressions": [
{
"operator": "OR",
"expressions": [
{"field": "email", "operator": "like", "value": "%@example.com"},
{"field": "created_at", "operator": "gte", "value": "2024-01-01"}
]
}
]
}
# Query params for pagination
params = {"page": 1, "pageSize": 50}
r = requests.post(f"{base}/api/user/filter", json=q, params=params, headers={
"Authorization": f"Bearer {token}"
})
r.raise_for_status()
print(r.json())
{
"expressions": [
{
"field": "email",
"operator": "like",
"value": "%@example.com",
"func_name": "LOWER"
},
{
"operator": "AND",
"expressions": [
{"field": "age", "operator": "gte", "value": 18},
{"field": "status", "operator": "in", "value": ["active", "pending"]}
]
}
]
}
{
"expressions": [
{"field": "status", "operator": "eq", "value": "active"}
],
"aggregations": {
"functions": [
{"type": "COUNT", "field": "id", "alias": "total_count"},
{"type": "SUM", "field": "amount", "alias": "total_amount"},
{"type": "AVG", "field": "rating", "alias": "avg_rating"}
],
"group_by": ["category", "region"]
}
}
{
"expressions": [
{"field": "active", "operator": "eq", "value": true},
{
"operator": "OR",
"expressions": [
{"field": "age", "operator": "lt", "value": 25},
{"field": "age", "operator": "gt", "value": 50}
]
},
{
"relationship": "addresses",
"include": true,
"expressions": [
{"field": "city", "operator": "like", "value": "New%"}
]
}
],
"projections": ["id", "name", "age", "active"]
}
// Returns active users who are either under 25 or over 50,
// and have at least one address in a city starting with "New"
Advanced Features
Available field functions: UPPER, LOWER, TRIM for text manipulation; SUM, AVG, MIN, MAX, COUNT for aggregations. Relationship filters support nested queries and selective field inclusion.
Relationship Filtering
Use RelationshipExpression to filter on related entities. The scope parameter controls filtering behavior:
"scope": "filterParent"(default) - Only returns parent entities that have matching child records. If no child matches, the parent is excluded."scope": "filterChild"- Returns all parent entities, but filters the included child records to only those matching the criteria.
{
"expressions": [
{
"relationship": "orders",
"expressions": [
{"field": "total", "operator": "gte", "value": 1000},
{
"relationship": "items",
"expressions": [
{"field": "quantity", "operator": "gt", "value": 5}
],
"include": false,
"scope": "filterParent"
}
],
"include": true,
"scope": "filterChild",
"projections": ["id", "total", "created_at"]
}
]
}
// Returns users with their orders (filtered to only orders >= 1000),
// where those orders have items with quantity > 5
Authentication
Use JWT or API key headers. Row‑Level Security (RLS) can reference JWT claims to enforce per‑model conditions. For multi‑tenant apps, include tenant IDs in tables and define RLS rules in the platform.
Authorization: Bearer <JWT>
X-API-Key: <API_KEY>
Real‑time
Connect to WebSocket endpoints for real-time updates. Available events: created, updated, deleted.
WebSocket Connection Examples
// WebSocket connection manager
class SwainWebSocket {
constructor(baseUrl, token) {
this.baseUrl = baseUrl.replace('https://', 'wss://').replace('http://', 'ws://');
this.token = token;
this.connections = {};
}
subscribe(model, event, callback) {
const url = `${this.baseUrl}/ws/${model}/${event}?token=${this.token}`;
const ws = new WebSocket(url);
ws.onopen = () => {
console.log(`Connected to ${model}/${event}`);
};
ws.onmessage = (e) => {
const msg = JSON.parse(e.data);
callback(msg.data ?? msg);
};
ws.onerror = (error) => {
console.error(`WebSocket error for ${model}/${event}:`, error);
};
ws.onclose = () => {
console.log(`Disconnected from ${model}/${event}`);
// Implement reconnection logic here
setTimeout(() => this.subscribe(model, event, callback), 5000);
};
this.connections[`${model}/${event}`] = ws;
return ws;
}
unsubscribe(model, event) {
const key = `${model}/${event}`;
if (this.connections[key]) {
this.connections[key].close();
delete this.connections[key];
}
}
closeAll() {
Object.values(this.connections).forEach(ws => ws.close());
this.connections = {};
}
}
// Usage
const ws = new SwainWebSocket('https://api.your-domain.com', 'YOUR_JWT_TOKEN');
// Subscribe to order events
ws.subscribe('orders', 'created', (order) => {
console.log('New order created:', order);
// Update UI with new order
});
ws.subscribe('orders', 'updated', (order) => {
console.log('Order updated:', order);
// Update existing order in UI
});
ws.subscribe('orders', 'deleted', (resp) => {
console.log('Order deleted:', resp.id);
// Remove order from UI
});
interface WebSocketMessage<T> {
event: 'created' | 'updated' | 'deleted';
data: T;
}
interface Order {
id: number;
customerId: number;
total: number;
status: string;
created_at: string;
}
class SwainWebSocket {
private baseUrl: string;
private token: string;
private connections: Map<string, WebSocket> = new Map();
private reconnectDelays: Map<string, number> = new Map();
constructor(baseUrl: string, token: string) {
this.baseUrl = baseUrl.replace('https://', 'wss://').replace('http://', 'ws://');
this.token = token;
}
subscribe<T>(
model: string,
event: 'created' | 'updated' | 'deleted',
callback: (data: T) => void
): WebSocket {
const key = `${model}/${event}`;
const url = `${this.baseUrl}/ws/${model}/${event}?token=${this.token}`;
// Close existing connection if any
this.unsubscribe(model, event);
const ws = new WebSocket(url);
ws.onopen = () => {
console.log(`✅ Connected to ${key}`);
this.reconnectDelays.set(key, 1000); // Reset delay
};
ws.onmessage = (e: MessageEvent) => {
try {
const message: WebSocketMessage<T> = JSON.parse(e.data);
callback(message.data);
} catch (error) {
console.error(`Failed to parse message for ${key}:`, error);
}
};
ws.onerror = (error: Event) => {
console.error(`❌ WebSocket error for ${key}:`, error);
};
ws.onclose = (e: CloseEvent) => {
console.log(`🔌 Disconnected from ${key} (code: ${e.code})`);
if (!e.wasClean) {
// Exponential backoff for reconnection
const delay = this.reconnectDelays.get(key) || 1000;
console.log(`Reconnecting in ${delay}ms...`);
setTimeout(() => {
this.subscribe(model, event, callback);
}, delay);
// Increase delay for next reconnection (max 30 seconds)
this.reconnectDelays.set(key, Math.min(delay * 2, 30000));
}
};
this.connections.set(key, ws);
return ws;
}
unsubscribe(model: string, event: string): void {
const key = `${model}/${event}`;
const ws = this.connections.get(key);
if (ws) {
ws.close(1000, 'Client closing connection');
this.connections.delete(key);
this.reconnectDelays.delete(key);
}
}
closeAll(): void {
this.connections.forEach((ws, key) => {
ws.close(1000, 'Client closing all connections');
});
this.connections.clear();
this.reconnectDelays.clear();
}
}
// Usage with TypeScript
const ws = new SwainWebSocket('https://api.your-domain.com', 'YOUR_JWT_TOKEN');
// Subscribe to typed order events
ws.subscribe<Order>('orders', 'created', (order) => {
console.log(`New order #${order.id} - Total: $${order.total}`);
});
ws.subscribe<Order>('orders', 'updated', (order) => {
console.log(`Order #${order.id} updated - Status: ${order.status}`);
});
ws.subscribe<{ id: number }>('orders', 'deleted', (data) => {
console.log(`Order #${data.id} was deleted`);
});
import asyncio
import json
import websockets
from typing import Callable, Dict, Optional
from dataclasses import dataclass
from datetime import datetime
@dataclass
class Order:
id: int
customer_id: int
total: float
status: str
created_at: str
class SwainWebSocket:
def __init__(self, base_url: str, token: str):
self.base_url = base_url.replace('https://', 'wss://').replace('http://', 'ws://')
self.token = token
self.connections: Dict[str, websockets.WebSocketClientProtocol] = {}
self.tasks: Dict[str, asyncio.Task] = {}
async def subscribe(
self,
model: str,
event: str,
callback: Callable,
retry_delay: int = 5
):
"""Subscribe to WebSocket events with automatic reconnection."""
key = f"{model}/{event}"
url = f"{self.base_url}/ws/{model}/{event}?token={self.token}"
while True:
try:
async with websockets.connect(url) as websocket:
self.connections[key] = websocket
print(f"✅ Connected to {key}")
async for message in websocket:
try:
payload = json.loads(message)
await callback(payload.get('data', payload))
except json.JSONDecodeError as e:
print(f"Failed to parse message: {e}")
except Exception as e:
print(f"Error in callback: {e}")
except websockets.exceptions.ConnectionClosed as e:
print(f"🔌 Connection closed for {key}: {e}")
if key in self.connections:
del self.connections[key]
except Exception as e:
print(f"❌ WebSocket error for {key}: {e}")
# Reconnection logic
print(f"Reconnecting to {key} in {retry_delay} seconds...")
await asyncio.sleep(retry_delay)
retry_delay = min(retry_delay * 2, 30) # Exponential backoff
async def unsubscribe(self, model: str, event: str):
"""Close a specific WebSocket connection."""
key = f"{model}/{event}"
if key in self.connections:
await self.connections[key].close()
del self.connections[key]
if key in self.tasks:
self.tasks[key].cancel()
del self.tasks[key]
async def close_all(self):
"""Close all WebSocket connections."""
for ws in self.connections.values():
await ws.close()
for task in self.tasks.values():
task.cancel()
self.connections.clear()
self.tasks.clear()
# Usage example
async def main():
ws = SwainWebSocket('https://api.your-domain.com', 'YOUR_JWT_TOKEN')
# Define callbacks
async def on_order_created(data):
order = Order(**data)
print(f"New order #{order.id} - Total: ${order.total}")
async def on_order_updated(data):
order = Order(**data)
print(f"Order #{order.id} updated - Status: {order.status}")
async def on_order_deleted(data):
print(f"Order #{data['id']} was deleted")
# Create subscription tasks
tasks = [
asyncio.create_task(ws.subscribe('orders', 'created', on_order_created)),
asyncio.create_task(ws.subscribe('orders', 'updated', on_order_updated)),
asyncio.create_task(ws.subscribe('orders', 'deleted', on_order_deleted))
]
try:
# Run all subscriptions concurrently
await asyncio.gather(*tasks)
except KeyboardInterrupt:
print("Shutting down...")
await ws.close_all()
# Run the async main function
if __name__ == "__main__":
asyncio.run(main())
import Foundation
// MARK: - Models
struct Order: Codable {
let id: Int
let customerId: Int
let total: Double
let status: String
let created_at: String
}
struct DeleteResponse: Codable {
let id: Int?
}
struct WebSocketMessage<T: Codable>: Codable {
let event: String
let data: T
}
// MARK: - WebSocket Manager
class SwainWebSocket: NSObject {
private let baseURL: String
private let token: String
private var connections: [String: URLSessionWebSocketTask] = [:]
private var session: URLSession!
init(baseURL: String, token: String) {
self.baseURL = baseURL
.replacingOccurrences(of: "https://", with: "wss://")
.replacingOccurrences(of: "http://", with: "ws://")
self.token = token
super.init()
self.session = URLSession(configuration: .default, delegate: self, delegateQueue: nil)
}
func subscribe<T: Codable>(
model: String,
event: String,
type: T.Type,
callback: @escaping (T) -> Void
) {
let key = "\(model)/\(event)"
let urlString = "\(baseURL)/ws/\(model)/\(event)?token=\(token)"
guard let url = URL(string: urlString) else {
print("Invalid URL: \(urlString)")
return
}
// Close existing connection if any
unsubscribe(model: model, event: event)
let webSocketTask = session.webSocketTask(with: url)
connections[key] = webSocketTask
// Start receiving messages
receiveMessage(key: key, type: type, callback: callback)
// Connect
webSocketTask.resume()
print("✅ Connecting to \(key)")
}
private func receiveMessage<T: Codable>(
key: String,
type: T.Type,
callback: @escaping (T) -> Void
) {
guard let task = connections[key] else { return }
task.receive { [weak self] result in
switch result {
case .success(let message):
switch message {
case .string(let text):
if let data = text.data(using: .utf8) {
do {
let decoded = try JSONDecoder().decode(
WebSocketMessage<T>.self,
from: data
)
DispatchQueue.main.async {
callback(decoded.data)
}
} catch {
print("Decoding error: \(error)")
}
}
case .data(let data):
do {
let decoded = try JSONDecoder().decode(
WebSocketMessage<T>.self,
from: data
)
DispatchQueue.main.async {
callback(decoded.data)
}
} catch {
print("Decoding error: \(error)")
}
@unknown default:
break
}
// Continue receiving messages
self?.receiveMessage(key: key, type: type, callback: callback)
case .failure(let error):
print("❌ WebSocket error for \(key): \(error)")
// Implement reconnection logic
DispatchQueue.global().asyncAfter(deadline: .now() + 5) { [weak self] in
let components = key.split(separator: "/")
if components.count == 2 {
self?.subscribe(
model: String(components[0]),
event: String(components[1]),
type: type,
callback: callback
)
}
}
}
}
}
func unsubscribe(model: String, event: String) {
let key = "\(model)/\(event)"
if let task = connections[key] {
task.cancel(with: .goingAway, reason: nil)
connections.removeValue(forKey: key)
}
}
func closeAll() {
connections.values.forEach { task in
task.cancel(with: .goingAway, reason: nil)
}
connections.removeAll()
}
}
// MARK: - URLSessionWebSocketDelegate
extension SwainWebSocket: URLSessionWebSocketDelegate {
func urlSession(
_ session: URLSession,
webSocketTask: URLSessionWebSocketTask,
didOpenWithProtocol protocol: String?
) {
print("✅ WebSocket connected")
}
func urlSession(
_ session: URLSession,
webSocketTask: URLSessionWebSocketTask,
didCloseWith closeCode: URLSessionWebSocketTask.CloseCode,
reason: Data?
) {
print("🔌 WebSocket disconnected with code: \(closeCode)")
}
}
// MARK: - Usage
class OrderManager {
let webSocket = SwainWebSocket(
baseURL: "https://api.your-domain.com",
token: "YOUR_JWT_TOKEN"
)
func startListening() {
// Subscribe to order events
webSocket.subscribe(
model: "orders",
event: "created",
type: Order.self
) { order in
print("New order #\(order.id) - Total: $\(order.total)")
// Update UI
}
webSocket.subscribe(
model: "orders",
event: "updated",
type: Order.self
) { order in
print("Order #\(order.id) updated - Status: \(order.status)")
// Update UI
}
webSocket.subscribe(
model: "orders",
event: "deleted",
type: DeleteResponse.self
) { data in
if let orderId = data.id {
print("Order #\(orderId) was deleted")
// Update UI
}
}
}
func stopListening() {
webSocket.closeAll()
}
}
import okhttp3.*
import com.google.gson.Gson
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.*
import java.util.concurrent.TimeUnit
// Data classes
data class Order(
val id: Int,
val customerId: Int,
val total: Double,
val status: String,
val created_at: String
)
data class WebSocketMessage<T>(
val event: String,
val data: T
)
// WebSocket Manager
class SwainWebSocket(
private val baseUrl: String,
private val token: String
) {
private val client = OkHttpClient.Builder()
.pingInterval(30, TimeUnit.SECONDS)
.build()
private val gson = Gson()
private val connections = mutableMapOf<String, WebSocket>()
private val reconnectDelays = mutableMapOf<String, Long>()
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
inline fun <reified T> subscribe(
model: String,
event: String,
crossinline callback: (T) -> Unit
): Flow<T> {
val key = "$model/$event"
val flow = Channel<T>(Channel.UNLIMITED)
val wsUrl = baseUrl
.replace("https://", "wss://")
.replace("http://", "ws://")
val url = "$wsUrl/ws/$model/$event?token=$token"
val request = Request.Builder()
.url(url)
.build()
val listener = object : WebSocketListener() {
override fun onOpen(webSocket: WebSocket, response: Response) {
super.onOpen(webSocket, response)
connections[key] = webSocket
reconnectDelays[key] = 1000L // Reset delay
println("✅ Connected to $key")
}
override fun onMessage(webSocket: WebSocket, text: String) {
super.onMessage(webSocket, text)
try {
val message = gson.fromJson(
text,
WebSocketMessage::class.java
)
val data = gson.fromJson(
gson.toJson(message.data),
T::class.java
)
scope.launch {
flow.send(data)
withContext(Dispatchers.Main) {
callback(data)
}
}
} catch (e: Exception) {
println("Failed to parse message: ${e.message}")
}
}
override fun onClosing(webSocket: WebSocket, code: Int, reason: String) {
super.onClosing(webSocket, code, reason)
webSocket.close(1000, null)
connections.remove(key)
println("🔌 Closing $key: $reason")
}
override fun onClosed(webSocket: WebSocket, code: Int, reason: String) {
super.onClosed(webSocket, code, reason)
connections.remove(key)
println("🔌 Closed $key: $reason")
}
override fun onFailure(
webSocket: WebSocket,
t: Throwable,
response: Response?
) {
super.onFailure(webSocket, t, response)
connections.remove(key)
println("❌ WebSocket error for $key: ${t.message}")
// Reconnection with exponential backoff
val delay = reconnectDelays[key] ?: 1000L
println("Reconnecting in ${delay}ms...")
scope.launch {
delay(delay)
subscribe<T>(model, event, callback)
}
// Increase delay for next reconnection (max 30 seconds)
reconnectDelays[key] = minOf(delay * 2, 30000L)
}
}
// Close existing connection if any
unsubscribe(model, event)
// Create new WebSocket connection
client.newWebSocket(request, listener)
return flow.receiveAsFlow()
}
fun unsubscribe(model: String, event: String) {
val key = "$model/$event"
connections[key]?.close(1000, "Client closing connection")
connections.remove(key)
reconnectDelays.remove(key)
}
fun closeAll() {
connections.values.forEach { ws ->
ws.close(1000, "Client closing all connections")
}
connections.clear()
reconnectDelays.clear()
scope.cancel()
}
}
// Usage in Android Activity/Fragment
class OrderActivity : AppCompatActivity() {
private lateinit var webSocket: SwainWebSocket
private val orders = mutableListOf<Order>()
private lateinit var adapter: OrderAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_orders)
webSocket = SwainWebSocket(
baseUrl = "https://api.your-domain.com",
token = "YOUR_JWT_TOKEN"
)
setupRecyclerView()
startListeningToOrders()
}
private fun startListeningToOrders() {
// Subscribe to order created events
lifecycleScope.launch {
webSocket.subscribe<Order>("orders", "created") { order ->
println("New order #${order.id} - Total: $${order.total}")
orders.add(order)
adapter.notifyItemInserted(orders.size - 1)
}.collect { order ->
// Flow-based processing
processNewOrder(order)
}
}
// Subscribe to order updated events
lifecycleScope.launch {
webSocket.subscribe<Order>("orders", "updated") { order ->
println("Order #${order.id} updated - Status: ${order.status}")
val index = orders.indexOfFirst { it.id == order.id }
if (index != -1) {
orders[index] = order
adapter.notifyItemChanged(index)
}
}.collect()
}
// Subscribe to order deleted events
lifecycleScope.launch {
webSocket.subscribe<Map<String, Int>>("orders", "deleted") { data ->
data["id"]?.let { orderId ->
println("Order #$orderId was deleted")
val index = orders.indexOfFirst { it.id == orderId }
if (index != -1) {
orders.removeAt(index)
adapter.notifyItemRemoved(index)
}
}
}.collect()
}
}
private fun processNewOrder(order: Order) {
// Additional processing logic
}
override fun onDestroy() {
super.onDestroy()
webSocket.closeAll()
}
}
Errors & validation
The API returns structured error responses with appropriate HTTP status codes. All error responses include a JSON object with a message field and optionally additional context. Input validation ensures type safety and proper operator usage. The API supports database transactions for data consistency - if any part of a request fails, the entire operation is rolled back.
HTTP Status Codes
| Status | Meaning | Common Causes |
|---|---|---|
200 | Success | Request completed successfully |
201 | Created | Resource created successfully |
204 | No Content | Not used; deletions return 200 with JSON |
400 | Bad Request | Invalid JSON, wrong data type, invalid filter syntax |
401 | Unauthorized | Missing or invalid authentication token |
403 | Forbidden | Valid auth but insufficient permissions (RLS violation) |
404 | Not Found | Resource doesn't exist or is inaccessible |
409 | Conflict | Unique constraint violation, concurrent update conflict |
422 | Unprocessable Entity | Validation error, business rule violation |
429 | Too Many Requests | Rate limit exceeded |
500 | Internal Server Error | Unexpected server error |
503 | Service Unavailable | Database connection lost, maintenance mode |
Error Response Format
{
"message": "Invalid operator: ILIKE not supported for field type"
}
{
"message": "Validation failed",
"errors": {
"email": "Invalid email format",
"age": "Must be a positive integer"
}
}
Common Error Scenarios
# Request without authentication
curl https://api.your-domain.com/api/user
{
"message": "Authentication required"
}
// User tries to access data outside their tenant scope
{
"message": "Access denied: Row-level security policy violation",
"resource": "orders",
"policy": "tenant_isolation"
}
// Attempting to create duplicate unique value
{
"message": "Unique constraint violation",
"field": "email",
"value": "user@example.com",
"constraint": "users_email_unique"
}
Input Validation
The API performs comprehensive validation before executing any database operations:
- Type Validation: Ensures field values match expected data types
- Required Fields: Checks that all non-nullable fields are provided
- Format Validation: Validates emails, URLs, dates, and other formatted data
- Range Validation: Enforces min/max values for numbers and string lengths
- Relationship Validation: Verifies foreign key references exist
- Custom Business Rules: Applies any model-specific validation logic
Transaction Handling
All write operations (CREATE, UPDATE, DELETE) are wrapped in database transactions. This ensures:
- Atomicity - all changes succeed or all fail
- Consistency - database constraints are maintained
- Isolation - concurrent requests don't interfere
- Durability - committed changes persist
Rate Limiting
APIs include rate limiting to prevent abuse. When limits are exceeded:
// Headers
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1640995200
Retry-After: 3600
{
"message": "Rate limit exceeded. Try again in 3600 seconds.",
"retry_after": 3600
}