southkin/fullyrestful
Most networking libraries make you split one API across multiple places:
β¨ What makes it different?
Most networking libraries make you split one API across multiple places:
- endpoint path
- request model
- response model
- request-building logic
FullyRESTful was built for the opposite approach:
- one API
- one Swift type
- request and response defined together
That means the shape your backend teammate sends you can stay almost κ·Έλλ‘ in code.
This library lets you define APIs like this:
let responseModel = try await MyAPI().request(param: .init(...)).model...instead of writing all this:
var request = URLRequest(url: URL(string: "...")!)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpBody = try JSONEncoder().encode(MyAPI.Request(...))
let (data, response) = try await URLSession.shared.data(for: request)
guard let httpResponse = response as? HTTPURLResponse,
(200...299).contains(httpResponse.statusCode) else {
throw URLError(.badServerResponse)
}
let responseModel = try JSONDecoder().decode(MyAPI.Response.self, from: data)π€ Why FullyRESTful?
- You want each API to own its
RequestandResponsetogether. - You do not want endpoint specs scattered across files.
- You want to turn backend API guides into Swift types with minimal glue code.
- You want REST and WebSocket to feel consistent.
- You want a small abstraction over
URLSession, not a giant framework.
FullyRESTful is a Swift library supporting both RESTful APIs and WebSocket connections in a simple, declarative, and composable way.
π¨ FullyRESTful 3.0.0
Breaking Changes in WebSocket Support
The internal WebSocket system has been completely restructured. If you were using WebSocket features in version 2.x, please update your usage accordingly.
π Features
- Declarative API Definitions
Define REST or WebSocket APIs in a single Swift structure.
- RESTful API Support
Simple request/response modeling with JSON encoding/decoding.
- Multipart Upload Support
Upload files and form data with minimal configuration.
- Modern WebSocket Support
Connect once and share the connection via Combine.
- Modular Design
Use only the features you need.
π¦ Installation
Swift Package Manager
dependencies: [
.package(url: "https://github.com/southkin/FullyRESTful.git", .upToNextMajor(from: "3.0.0"))
]π RESTful API Usage
Define a Server
let myServer = ServerInfo(domain: "https://api.example.com", defaultHeader: [:])Define an API
struct MyAPI: APIITEM {
var server = myServer
struct Request: Codable {
let param1: String?
let param2: [Int]
let param3: [String: Float]
}
struct Response: Codable {
let result1: [String]
let result2: [Int]?
let result3: [String: Float]?
}
var requestModel = Request.self
var responseModel = Response.self
var method: HTTPMethod = .POST
var path: String = "/myapi/path"
}Request
let data = try? await MyAPI().getData(param: .init(param1: "example", param2: [1, 2, 3], param3: ["key": 1.23])).data
let model = try await MyAPI().request(
param: .init(param1: "example", param2: [1, 2, 3], param3: ["key": 1.23])
).modelβ Empty Response
For endpoints like DELETE /resource/:id that return 204 No Content:
struct DeleteUserAPI: APIITEM {
var server = myServer
struct Request: Codable {}
var requestModel = Request.self
var responseModel = EmptyResponse.self
var method: HTTPMethod = .DELETE
var path: String = "/users/1"
}
let response = try await DeleteUserAPI().request(param: .init())
print(response.rawResponse.statusCode)π Plain Text Response
If the server returns text/plain, use String as the response model:
struct HealthCheckAPI: APIITEM {
var server = myServer
struct Request: Codable {}
var requestModel = Request.self
var responseModel = String.self
var method: HTTPMethod = .GET
var path: String = "/health"
}
let text = try await HealthCheckAPI().request(param: .init()).modelπ― Custom Decoder
If the backend needs custom date decoding or key strategies:
struct EventAPI: APIITEM {
var server = myServer
struct Request: Codable {}
struct Response: Codable {
let createdAt: Date
}
var requestModel = Request.self
var responseModel = Response.self
var method: HTTPMethod = .GET
var path: String = "/event"
var decoder: JSONDecoder {
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
return decoder
}
}π Multipart Upload
Define an API with File Upload
struct MyUploadAPI: APIITEM, MultipartUpload {
var server = myServer
struct Request: Codable {
let title: String
let image: MultipartItem
}
struct Response: Codable {
let uploadedURL: String
}
var requestModel = Request.self
var responseModel = Response.self
var method: HTTPMethod = .POST
var path: String = "/upload"
}π WebSocket Usage
WebSockets in FullyRESTful are:
- automatically managed (single connection per endpoint)
- Combine-based, and cancellable
- pinged at regular intervals to stay alive
- capable of sending .text, .binary, or .codable(...) payloads
Define a WebSocket
struct EchoSocket: WebSocketITEM {
var server = ServerInfo(domain: "wss://echo.websocket.org", defaultHeader: [:])
var path: String = ""
// Optional: override ping interval (default = 10 sec)
var pingInterval: TimeInterval { 5 }
}Connect and Subscribe
let socket = EchoSocket()
socket.listen()
.sink { message in
print("π₯", message)
}
.store(in: &cancellables)Send Text Message
try await socket.send(.text("Hello WebSocket!"))Send a Codable Message
struct ChatMessage: Codable {
let type: String
let content: String
}
let message = ChatMessage(type: "chat", content: "Hello from Codable!")
try await socket.send(.codable(message))Decode Received Messages
socket.listen()
.decode(ChatMessage.self)
.sink { decoded in
print("π© Decoded:", decoded)
}
.store(in: &cancellables)π§ͺ Debugging
Enable cURL log output for debugging:
struct MyAPI: APIITEM {
var curlLog = true
}You can also inject a custom URLSession for tests or controlled environments:
struct MyAPI: APIITEM {
var server = myServer
var session: URLSession = .shared
}β οΈ Error Handling
FullyRESTful throws FullyRESTfulError for the common transport and decoding cases.
Typical cases:
badURL: invalidserver.domain + pathhttpError(statusCode:data:): non-success HTTP response with raw body preservedemptyResponseBody: body was empty but yourResponseModelcould not decode from itunsupportedContentType: response was not JSON or textinvalidWebSocketState: send attempted before a valid socket state existed
Example:
do {
let response = try await MyAPI().request(param: .init(...))
print(response.model)
} catch let error as FullyRESTfulError {
switch error {
case .httpError(let statusCode, let data):
print("status:", statusCode)
print("body:", String(data: data, encoding: .utf8) ?? "")
default:
print(error.localizedDescription)
}
}π License
MIT License
Package Metadata
Repository: southkin/fullyrestful
Default branch: main
README: README.md