Skip to content

Instantly share code, notes, and snippets.

@thomsmed
Last active April 1, 2026 10:40
Show Gist options
  • Select an option

  • Save thomsmed/76aac9adf5257312156a0691859a259a to your computer and use it in GitHub Desktop.

Select an option

Save thomsmed/76aac9adf5257312156a0691859a259a to your computer and use it in GitHub Desktop.
Showcasing the idea of an extendable and modular Endpoint type. Heavily inspired by [TinyNetworking](https://github.com/objcio/tiny-networking).
//
// Endpoint.playground
//
// Showcasing the idea of an Endpoint type (heavily inspired by [TinyNetworking](https://github.com/objcio/tiny-networking)).
// Endpoint is an extendable and modular type (struct) holding all information (and logic) needed in order to prepare a HTTP request, launch a HTTP request, and process the returned HTTP response.
//
import Foundation
import PlaygroundSupport
PlaygroundPage.current.needsIndefiniteExecution = true
struct UnexpectedResponse: Error {
let response: HTTPURLResponse
let data: Data
}
struct RequestPayload {
let mimeType: String?
let body: Data?
}
struct ResponseParser<Value> {
let mimeType: String?
let parse: (HTTPURLResponse, Data) throws -> Value
}
extension RequestPayload {
/// Empty HTTP Request Payload.
static func empty() -> RequestPayload {
RequestPayload(mimeType: nil, body: nil)
}
/// JSON HTTP Request Payload.
static func json<T: Encodable>(encoding value: T) throws -> RequestPayload {
let encoder = JSONEncoder()
let data = try encoder.encode(value)
return RequestPayload(mimeType: "application/json", body: data)
}
// ...other shared request payloads goes here...
}
extension ResponseParser {
/// HTTP Response Parser that ignores any HTTP Response body, and just returns `Void`.
static func void(
expecting expectedStatusCodes: [Int] = [200]
) -> ResponseParser<Void> {
ResponseParser<Void>(mimeType: nil) { response, data in
guard expectedStatusCodes.contains(response.statusCode) else {
throw UnexpectedResponse(response: response, data: data)
}
}
}
/// HTTP Response Parser that tries to parse any HTTP Response body as JSON.
static func json<T: Decodable>(
expecting expectedStatusCodes: [Int] = [200, 201]
) -> ResponseParser<T> {
ResponseParser<T>(mimeType: "application/json") { response, data in
guard expectedStatusCodes.contains(response.statusCode) else {
throw UnexpectedResponse(response: response, data: data)
}
let decoder = JSONDecoder()
return try decoder.decode(T.self, from: data)
}
}
// ...other shared response parsers goes here...
}
/// A type that holds everything needed in order to prepare a HTTP request, send it, and process the received HTTP response.
struct Endpoint<Resource> {
public let url: URL
public let method: String
public let payload: RequestPayload
public let parser: ResponseParser<Resource>
// ...additional data goes here (details about authentication++)...
}
extension Endpoint {
/// Utility method for mapping raw response types to more relevant domain types.
func map<MappedResource>(
_ transform: @escaping (Resource) -> MappedResource
) -> Endpoint<MappedResource> {
Endpoint<MappedResource>(
url: self.url,
method: self.method,
payload: self.payload,
parser: ResponseParser(
mimeType: self.parser.mimeType
) { response, data in
transform(try self.parser.parse(response, data))
}
)
}
}
/// Only one abstraction layer around networking - basically just a simple function.
protocol HTTPClient {
func call<Resource>(_ endpoint: Endpoint<Resource>) async throws -> Resource
}
/// For the simplest and most straight forward implementation of `HTTPClient` - just extend Foundation Framework's `URLSession`!
extension URLSession: HTTPClient {
func call<Resource>(_ endpoint: Endpoint<Resource>) async throws -> Resource {
var request = URLRequest(url: endpoint.url)
request.httpMethod = endpoint.method
if let contentType = endpoint.payload.mimeType {
request.setValue(contentType, forHTTPHeaderField: "Content-Type")
}
if let accept = endpoint.parser.mimeType {
request.setValue(accept, forHTTPHeaderField: "Accept")
}
request.httpBody = endpoint.payload.body
let (data, response) = try await data(for: request) as! (Data, HTTPURLResponse)
return try endpoint.parser.parse(response, data)
}
}
// Usage - defining and calling Endpoints
// Free and open REST API for testing: https://jsonplaceholder.typicode.com/
struct Item: Codable {
let id: Int?
var title: String
}
/// Attach factory methods for Endpoints to types of relevance.
extension Item {
static func all() -> Endpoint<[Item]> {
Endpoint(
url: URL(string: "https://jsonplaceholder.typicode.com/todos")!,
method: "GET",
payload: .empty(),
parser: .json()
)
}
static func with(id: Int) -> Endpoint<Item> {
struct Todo: Decodable {
let id: Int
let userId: Int
let title: String
let completed: Bool
}
return Endpoint<Todo>(
url: URL(string: "https://jsonplaceholder.typicode.com/todos/\(id)")!,
method: "GET",
payload: .empty(),
parser: .json()
).map { todo in
Item(id: todo.id, title: todo.title)
}
}
func save() throws -> Endpoint<Item> {
if let id = self.id {
Endpoint(
url: URL(string: "https://jsonplaceholder.typicode.com/todos/\(id)")!,
method: "PUT",
payload: try .json(encoding: self),
parser: .json()
)
} else {
Endpoint(
url: URL(string: "https://jsonplaceholder.typicode.com/todos")!,
method: "POST",
payload: try .json(encoding: self),
parser: .json()
)
}
}
// ...other endpoint factory methods goes here...
}
/// Alternatively, attach factory methods for Endpoints to dedicated "namespaces".
enum ItemEndpoints {
static func all() -> Endpoint<[Item]> {
Endpoint(
url: URL(string: "https://jsonplaceholder.typicode.com/todos")!,
method: "GET",
payload: .empty(),
parser: .json()
)
}
static func with(id: Int) -> Endpoint<Item> {
struct Todo: Decodable {
let id: Int
let userId: Int
let title: String
let completed: Bool
}
return Endpoint<Todo>(
url: URL(string: "https://jsonplaceholder.typicode.com/todos/\(id)")!,
method: "GET",
payload: .empty(),
parser: .json()
).map { todo in
Item(id: todo.id, title: todo.title)
}
}
static func save(_ item: Item) throws -> Endpoint<Item> {
if let id = item.id {
Endpoint(
url: URL(string: "https://jsonplaceholder.typicode.com/todos/\(id)")!,
method: "PUT",
payload: try .json(encoding: item),
parser: .json()
)
} else {
Endpoint(
url: URL(string: "https://jsonplaceholder.typicode.com/todos")!,
method: "POST",
payload: try .json(encoding: item),
parser: .json()
)
}
}
}
Task {
do {
let httpClient: HTTPClient = URLSession.shared
var allItemsEndpoint = Endpoint<[Item]>(
url: URL(string: "https://jsonplaceholder.typicode.com/todos")!,
method: "GET",
payload: .empty(),
parser: .json()
) // Construct Endpoints inline
allItemsEndpoint = Item.all() // Construct Endpoints using factory methods on the type of interest
allItemsEndpoint = ItemEndpoints.all() // Construct Endpoints using factory methods under dedicated "namespace"
let items = try await httpClient.call(allItemsEndpoint)
print("Items:", items)
// ...
var newItem = Item(id: nil, title: "New Item")
newItem = try await httpClient.call(newItem.save())
print("New item:", newItem)
// ...
let previousItemId = newItem.id! - 1
let previousItem = try await httpClient.call(Item.with(id: previousItemId))
print("Previous item:", previousItem)
// ...
var changedItem = previousItem
changedItem.title = "Updated Item"
let updatedItem = try await httpClient.call(changedItem.save())
print("Updated item:", updatedItem)
} catch let unexpectedResponse as UnexpectedResponse {
print("Unexpected response:", unexpectedResponse)
print("Unexpected response (body):", String(data: unexpectedResponse.data, encoding: .utf8) ?? "")
} catch {
print("Error:", error)
}
PlaygroundPage.current.finishExecution()
}
// End
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment