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
- Vapor Integration - Set up the server-side API
- Predicates - Build complex queries
- Defining Models - Share models between client and server