A pluggable, dependency-free Swift paginator for bearer-authenticated REST APIs.
Documentation | Swift Package Index
.package(url: "https://github.com/adamtheturtle/PaginatedRESTClient.git", from: "0.1.0")Add the PaginatedRESTClient product to your target dependencies.
Provide a page model, an error mapping, and decoder/encoder factories. The default
transport is URLSessionTransport.
import Foundation
import PaginatedRESTClient
struct Item: Decodable, Sendable {
let id: Int
}
nonisolated struct ItemsPage: PagedResponse {
let items: [Item]
let nextPage: String?
let total: Int?
var pageItems: [Item] { items }
static func identity(of item: Item) -> AnyHashable? { item.id }
enum CodingKeys: String, CodingKey {
case items
case nextPage = "next_page"
case total
}
}
enum APIError: Error {
case missingKey, http(Int), decode(String), offline
}
struct APIErrors: RESTTransportErrorMapping {
nonisolated func missingAPIKey() -> Error { APIError.missingKey }
nonisolated func http(status: Int, body _: String) -> Error { APIError.http(status) }
nonisolated func decode(_ detail: String) -> Error { APIError.decode(detail) }
nonisolated func network(_: URLError) -> Error { APIError.offline }
nonisolated func isTransient(_ error: Error) -> Bool {
if case let APIError.http(code) = error {
return code == 429 || (500 ... 599).contains(code)
}
if case APIError.offline = error { return true }
return false
}
}
let client = try PaginatedRESTClient(
apiKey: token,
baseURL: URL(string: "https://api.example.com")!,
decoderFactory: { JSONDecoder() },
encoderFactory: { JSONEncoder() },
errors: APIErrors()
)
let items = try await client.fetchAllPages(ItemsPage.self, path: "/items/")See the DocC overview for the full API, and Custom transports for Get and Alamofire adapters.
URLSessionTransport (the default) reads at most 10 MiB of a 2xx response body and
64 KiB for any other status code. The limit is chosen from the HTTP status as the
response arrives. Bodies that exceed the applicable limit throw RESTResponseTooLargeError
(with overflow phase and size metadata). PaginatedRESTClient maps a 2xx overflow through
your error mapping's decode(_:), and a non-2xx overflow through http(status:body:), so a
large 401/404/429/500 keeps its HTTP classification. Pass successResponseLimit and
errorResponseLimit to URLSessionTransport when you need different values.
Pagination is bounded per client. defaultMaxSequentialPages and defaultMaxParallelPages
both default to 1000. Override them with maxSequentialPages and maxParallelPages on
PaginatedRESTClient's initializer. Exceeding either cap throws through your error mapping's
invalidRequest(_:) rather than silently truncating. The sequential walk counts the first
page toward maxSequentialPages; the parallel path derives its page count from total and
PagedResponse.pageSize. A negative total is rejected via decode(_:).
Request-body mistakes before the transport runs (for example supplying both an in-memory
body and a file URL) throw RESTRequestBodyError, which PaginatedRESTClient maps through
invalidRequest(_:).
PaginatedRESTClient: Fetch, stream, retry with shared rate-limit-aware backoff, decode, and de-duplicate paginated REST endpoints through a pluggable transport.
- Swift 6.2+
- macOS 15+, iOS 18+, tvOS 18+, watchOS 11+, or visionOS 2+
MIT. See LICENSE.