Skip to content

Defining Endpoints

Husnain Ali edited this page Sep 27, 2026 · 3 revisions

Defining Endpoints

Endpoint has one required member, associatedtype Response, and one commonly overridden member, path. Everything else has a default.

struct GetUser: Endpoint {
    typealias Response = User

    let id: Int
    var path: String { "/users/\(id)" }
}

struct SearchUsers: Endpoint {
    typealias Response = [User]

    let query: String
    var path: String { "/users" }
    var queryParameters: QueryParameters? { ["q": .string(query), "limit": .int(20)] }
}

struct UpdateProfile: Endpoint {
    typealias Response = User

    let draft: ProfileDraft
    var method: HTTPMethod { .patch }
    var path: String { "/me" }
    var body: RequestBody? { .json(draft) }
    var authentication: AuthRequirement { .required }
}

Path parameters are templated with :name and filled from pathParameters:

struct GetPost: Endpoint {
    typealias Response = Post
    let userID: Int
    let postID: Int
    var path: String { "/users/:userID/posts/:postID" }
    var pathParameters: [String: String] { ["userID": "\(userID)", "postID": "\(postID)"] }
}

Grouping operations in an enum works too:

enum UserAPI {
    struct Profile: Endpoint { typealias Response = User; var path: String { "/me" } }
    struct Update: Endpoint {
        typealias Response = User
        let draft: ProfileDraft
        var method: HTTPMethod { .patch }
        var path: String { "/me" }
        var body: RequestBody? { .json(draft) }
    }
}

Overridable members

Member Default Purpose
baseURL nil (use the environment's) Point one endpoint at a different host
method .get HTTP method
headers [:] Per-endpoint headers, merged over the environment's
queryParameters nil Typed query string
pathParameters [:] :name template substitutions
body nil Request body
authentication .none .none / .required / .custom(AuthStrategy)
timeout nil (use the environment's) Per-endpoint timeout
priority .normal Queue ordering when concurrency-limited
retryPolicy nil (use the config's) Per-endpoint retry override
cachePolicy nil (use the config's) Per-endpoint cache override
deduplicate nil (use the config's) Force dedup on/off for this endpoint
skipRequestQueue false Bypass the concurrency queue (use for the refresh call)
offlineBehavior .fail .fail or .queue(expiresAfter:)
decoder nil (use the config's) Per-endpoint JSONDecoder
decode(_:response:using:) generic dispatch Fully custom decoding

Request configuration

Configuration is layered: environment defaults → endpoint overrides → call site.

var config = NetworkConfiguration(
    baseURL: "https://api.example.com",
    headers: ["Accept": "application/json"],
    timeout: 30
)
config.retry = .aggressive
config.maxConcurrentRequests = 4
config.enableDeduplication = true
config.cache = .memory(policy: .networkFirst)
config.metrics = InMemoryMetrics()
config.environment.logLevel = .basic

let client = NetworkClient(configuration: config)
Concern Where
Base URL, default headers, timeout, log level NetworkEnvironment (via NetworkConfiguration)
Path / query / per-endpoint headers / body Endpoint
Cache policy config.cache default, Endpoint.cachePolicy override
Retry policy config.retry default, Endpoint.retryPolicy override
Authentication config.authorization + Endpoint.authentication
Pinning config.sslPinning
Concurrency limit config.maxConcurrentRequests

Request body

RequestBody cases:

var body: RequestBody? { .json(draft) }                        // Encodable -> application/json
var body: RequestBody? { .data(rawBytes, contentType: "application/octet-stream") }
var body: RequestBody? { .string("hello", contentType: "text/plain") }
var body: RequestBody? { .formURLEncoded(["grant_type": "refresh_token", "token": t]) }
var body: RequestBody? { .multipart(form) }                    // see Uploads, Downloads and Progress

.json encodes with the configuration's defaultEncoder (or the endpoint's), which by default converts to snake_case and encodes dates as ISO 8601. Override NetworkConfiguration.defaultEncoder for different conventions.

Response handling

Response can be:

Response type Behavior
Decodable / Codable Decoded with the endpoint or configuration JSONDecoder
Data Raw bytes, no decoding
String UTF-8 decoded body
EmptyResponse For 204 / empty-body endpoints

Status handling is centralized: 2xx succeeds; 401 → .unauthorized, 403 → .forbidden, 404 → .notFound, 422 → .validation, 429 → .rateLimited(retryAfter:), 5xx → .server, anything else unexpected → .unacceptableStatusCode(code, context). Every error case carries a ResponseContext (status, headers, body, decoded server message) where one exists. See Error Handling.

Custom decoding for a non-JSON API:

struct GetProtobufThing: Endpoint {
    typealias Response = Thing
    var path: String { "/thing" }
    func decode(_ data: Data, response: HTTPURLResponse, using decoder: JSONDecoder) throws -> Thing {
        try Thing(serializedBytes: data)
    }
}

Clone this wiki locally