REST API Backend

The REST backend speaks to a remote HTTP API instead of a database. Use the same SwiftData API you know on the client, with any REST service handling persistence.

Installation

Add the REST backend to your target:

.product(name: "RESTBackend", package: "SwiftDataServer.SDK")

Configuration

The backend is configured with a RESTConfiguration:

import SwiftDataServer
import RESTBackend

let backend = RESTBackend(configuration: RESTConfiguration(
    baseURL: URL(string: "https://api.example.com/v1")!
))

let container = try ModelContainer(for: User.self, Post.self)
try await container.connect(to: backend)

Authentication

Authentication is applied per-request via the authenticationHandler; fixed headers go in defaultHeaders:

// Bearer token authentication
let backend = RESTBackend(configuration: RESTConfiguration(
    baseURL: URL(string: "https://api.example.com/v1")!,
    authenticationHandler: { request in
        request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
    }
))

// Fixed custom headers on every request
let backend = RESTBackend(configuration: RESTConfiguration(
    baseURL: URL(string: "https://api.example.com/v1")!,
    defaultHeaders: ["X-API-Key": "your-api-key"]
))

// Or configure from environment variables
// (REST_API_URL, REST_API_BEARER_TOKEN, REST_API_TIMEOUT)
let config = RESTConfiguration.fromEnvironment()

Usage

Once configured, use the standard SwiftData APIs:

let context = container.mainContext

// Fetch all users
let users = try await context.fetchAsync(FetchDescriptor<User>())

// Create a new user
let user = User(name: "Alice", email: "alice@example.com")
context.insert(user)
try await context.saveAsync()

// Query with predicates
let predicate = #Predicate<User> { $0.age > 18 }
let adults = try await context.fetchAsync(
    FetchDescriptor<User>(predicate: predicate)
)

Server Setup

The REST backend is a client: it translates SwiftData operations into HTTP calls against a REST API you host. By default it maps each model to its table name — GET/POST/PUT/DELETE /users, /posts, and so on (configurable via endpointStrategy). Build those routes with standard Vapor controllers backed by SwiftDataServer:

import Vapor
import SwiftDataServer
import VaporIntegration

// Server: expose your models over REST with normal Vapor routes
func routes(_ app: Application) throws {
    app.get("users") { req in
        try await req.fetchAll(User.self)
    }
    // ... POST /users, PUT /users/:id, DELETE /users/:id ...
}

The bundled sample project includes a complete server (Sample/Server) and a REST client (Sample/RESTClient) showing the full round trip.

Error Handling

REST-specific failures surface as RESTError:

do {
    let users = try await context.fetchAsync(FetchDescriptor<User>())
} catch let error as RESTError {
    switch error {
    case .networkError(let underlying):
        print("Network error: \(underlying)")
    case .authenticationFailed:
        print("Invalid or expired credentials")
    case .httpError(let statusCode, let body):
        print("Server returned \(statusCode)")
    case .timeout:
        print("Request timed out")
    default:
        print("REST error: \(error)")
    }
}

Limitations

  • Schema migrations are no-ops — the server owns the schema.
  • Requires network connectivity; there is no built-in offline cache.

Requirements

  • iOS 17+ / macOS 14+
  • A REST API exposing your models (see the bundled sample server)
  • Network connectivity

Next Steps