Networking & APIs

The Best SwiftUI Networking Libraries for 2026

Since async/await landed, native URLSession does most of what apps need. Here is an honest ranking of when to stay native and when Alamofire, Moya, Get, or Pulse actually earn a place in your project.

Last updated: 2026-07-17 7 min read By Ahmed Gagan, iOS Engineer
Quick Answer

In 2026, native URLSession with async/await is the right default for most SwiftUI apps — it is powerful, dependency-free, and the async API removed most of the old boilerplate. Add Alamofire when you need advanced features like request retry, multipart uploads, and interceptors out of the box. Get by Kean is the leanest way to build a typed API client, Moya suits teams that want an enum-defined API layer, and Pulse is not a networking library at all but the best way to debug and log your network traffic. Start native, add tools deliberately.

Best default
URLSession + async/await
Best full-featured
Alamofire
Best typed client
Get
Best debugging
Pulse

6 SwiftUI Networking Patterns, Live

Networking has no look of its own, so each preview shows the state the code produces — retries backing off, a request cancelling when the query changes, the four states a screen can be in. Every one of these is plain URLSession: no dependency required.

GET/posts200
GET/posts/42200

Async GET with URLSession

iOS 16.0

The modern baseline. No Alamofire needed for this, and most apps never need more.

struct APIClient {
    var baseURL: URL
    var session: URLSession = .shared

    func get<T: Decodable>(_ path: String, as type: T.Type = T.self) async throws -> T {
        let (data, response) = try await session.data(from: baseURL.appending(path: path))

        guard let http = response as? HTTPURLResponse else { throw APIError.badResponse }
        guard (200..<300).contains(http.statusCode) else {
            throw APIError.status(http.statusCode)
        }

        let decoder = JSONDecoder()
        decoder.keyDecodingStrategy = .convertFromSnakeCase
        return try decoder.decode(T.self, from: data)
    }
}

enum APIError: Error {
    case badResponse
    case status(Int)
}
POST/posts201
Content-Type: application/json

POST with a Codable Body

iOS 16.0

Encode, set the header, check the status — the four lines people forget.

extension APIClient {
    func post<Body: Encodable, T: Decodable>(
        _ path: String,
        body: Body,
        as type: T.Type = T.self
    ) async throws -> T {
        var request = URLRequest(url: baseURL.appending(path: path))
        request.httpMethod = "POST"
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")

        let encoder = JSONEncoder()
        encoder.keyEncodingStrategy = .convertToSnakeCase
        request.httpBody = try encoder.encode(body)

        let (data, response) = try await session.data(for: request)
        guard let http = response as? HTTPURLResponse,
              (200..<300).contains(http.statusCode) else {
            throw APIError.badResponse
        }
        return try JSONDecoder().decode(T.self, from: data)
    }
}
GET/posts · retry in 300ms503
GET/posts · retry in 600ms503
GET/posts · ok200

Retry with Backoff

iOS 16.0

Exponential backoff that retries transient failures and gives up on 4xx.

func withRetry<T>(
    attempts: Int = 3,
    initialDelay: Duration = .milliseconds(300),
    operation: () async throws -> T
) async throws -> T {
    var delay = initialDelay

    for attempt in 1...attempts {
        do {
            return try await operation()
        } catch let error as APIError {
            // A 4xx will fail identically next time — do not burn retries on it.
            if case .status(let code) = error, (400..<500).contains(code) { throw error }
            if attempt == attempts { throw error }
        } catch {
            if attempt == attempts { throw error }
        }
        try await Task.sleep(for: delay)
        delay *= 2
    }

    throw APIError.badResponse
}
.loading

Loadable View State

iOS 16.0

One enum that makes empty, loading, error and loaded impossible to conflate.

enum Loadable<Value> {
    case idle
    case loading
    case loaded(Value)
    case failed(Error)
}

@MainActor
final class FeedViewModel: ObservableObject {
    @Published private(set) var state: Loadable<[Post]> = .idle
    private let api: APIClient

    init(api: APIClient) { self.api = api }

    func load() async {
        state = .loading
        do {
            state = .loaded(try await api.get("/posts", as: [Post].self))
        } catch {
            state = .failed(error)
        }
    }
}

struct FeedView: View {
    @StateObject var vm: FeedViewModel

    var body: some View {
        Group {
            switch vm.state {
            case .idle, .loading:
                ProgressView()
            case .loaded(let posts) where posts.isEmpty:
                EmptyStateView(icon: "tray", title: "Nothing yet", message: "New posts appear here.")
            case .loaded(let posts):
                List(posts) { Text($0.title) }
            case .failed(let error):
                ErrorStateView(message: error.localizedDescription) { Task { await vm.load() } }
            }
        }
        .task { await vm.load() }
    }
}
s|
/search?q=sin flight
×/search?q=swcancelled
×/search?q=swicancelled
×/search?q=swiftcancelled

Cancellation with .task

iOS 16.0

task(id:) cancels the in-flight request when the input changes — search, solved.

struct SearchScreen: View {
    @State private var query = ""
    @State private var results: [Post] = []

    var body: some View {
        List(results) { Text($0.title) }
            .searchable(text: $query)
            // Re-runs on every query change AND cancels the previous request,
            // which is what stops results arriving out of order.
            .task(id: query) {
                guard query.count >= 2 else { results = []; return }
                do {
                    try await Task.sleep(for: .milliseconds(250))   // debounce
                    results = try await api.get("/search?q=(query)", as: [Post].self)
                } catch {
                    // CancellationError is expected here — ignore it.
                }
            }
    }
}
POST/upload · multipart
photo.jpg · 0%

Multipart Upload

iOS 16.0

Image upload without a dependency — the boundary handling people get wrong.

func upload(image: Data, to url: URL, fileName: String = "photo.jpg") async throws {
    let boundary = "Boundary-(UUID().uuidString)"
    var request = URLRequest(url: url)
    request.httpMethod = "POST"
    request.setValue("multipart/form-data; boundary=(boundary)",
                     forHTTPHeaderField: "Content-Type")

    var body = Data()
    body.append("--(boundary)
".data(using: .utf8)!)
    body.append("Content-Disposition: form-data; name="file"; filename="(fileName)"
"
        .data(using: .utf8)!)
    body.append("Content-Type: image/jpeg

".data(using: .utf8)!)
    body.append(image)
    body.append("
--(boundary)--
".data(using: .utf8)!)   // trailing -- is required

    let (_, response) = try await URLSession.shared.upload(for: request, from: body)
    guard let http = response as? HTTPURLResponse,
          (200..<300).contains(http.statusCode) else { throw APIError.badResponse }
}
Free download

6 SwiftUI networking recipes as one Swift file

A typed API client, retry with backoff, the Loadable state enum, cancellation via task(id:) and multipart upload — all plain URLSession, no dependency.

  • Typed async/await API client
  • Retry with exponential backoff that skips 4xx
  • The Loadable enum that kills empty-state bugs
  • MIT licensed, commercial use fine

Instant download, no confirmation step. Occasional SwiftUI tips — unsubscribe anytime.

Do you actually need a networking library?

For a typical REST app, native URLSession plus async/await plus a thin generic request function is all you need, and it will never break on an Xcode update. Reach for Alamofire when auth-refresh interceptors, retries, and multipart uploads would otherwise be significant hand-rolled code. Use Get if you want a typed client with minimal weight, Moya if your team values a rigid enum API contract, and add Pulse regardless of your client whenever you need to see what is actually going over the wire.

  • Simple REST + Codable: native URLSession
  • Retries, interceptors, uploads out of the box: Alamofire
  • Lightweight typed client: Get
  • Structured, testable enum API: Moya
  • Debugging traffic: add Pulse to any of the above

Ship your SwiftUI app in 5 emails

A free 5-part course on the parts that actually stall launches: paywall, auth, onboarding, App Store review, and pricing. No fluff, unsubscribe anytime.

A clean networking layer, already built

The Swift Kit ships a modern URLSession-based networking layer with async/await and Codable decoding, plus AI provider clients, so you call APIs from feature code without wiring the plumbing yourself.

Get The Swift Kit — $99

Short on time?

I can set the kit up for your app and hand back a running Xcode project — from $499. Source code still yours.

See done-for-you

5 SwiftUI networking tools worth knowing

Only some of these compete with each other — Pulse is complementary, and Get and Moya solve the API-layer problem differently. Here is where each genuinely fits.

  1. 1

    URLSession + async/await

    Best default

    Since Swift's concurrency model matured, URLSession's async methods let you write clean, linear networking code with try await, Codable decoding, and structured concurrency — no dependency required for the vast majority of REST APIs.

    Pros
    • Zero dependency, first-party, future-proof
    • async/await removed most old boilerplate
    • Works perfectly with Codable and structured concurrency
    • Full control over caching and configuration
    Cons
    • Advanced retry/interceptor logic is DIY
    • Multipart and complex uploads are verbose
    • You build your own thin client layer
    • Repetitive across many endpoints
    Learn more
  2. 2

    Alamofire

    Full-featured

    Alamofire is the long-standing, battle-tested HTTP networking library for Swift. It layers request/response validation, retries, interceptors, multipart uploads, and reachability on top of URLSession with a fluent API.

    Pros
    • Mature, stable, and extremely well-documented
    • Request retry, interceptors, and validation built in
    • Great for complex uploads and auth refresh flows
    • Huge community and Stack Overflow coverage
    Cons
    • Often overkill now that URLSession is async
    • A sizable dependency for simple apps
    • Adds an abstraction layer to learn
    • Most of its wins can be hand-rolled for basic needs
  3. 3

    Get (by Kean)

    Best typed client

    Get is a modern, minimal web API client by Alexander Grebenyuk built entirely on async/await. It gives you a typed, ergonomic client with request definitions and Codable decoding while staying far lighter than Alamofire.

    Pros
    • Modern async/await-first design
    • Typed, ergonomic API client with little code
    • Much lighter than Alamofire
    • Great pairing with Pulse for logging
    Cons
    • Fewer batteries than Alamofire
    • Smaller community
    • You may still add pieces for advanced needs
    • Another dependency versus pure URLSession
  4. 4

    Moya

    Enum-defined API layer

    Moya is an abstraction over Alamofire that models your API as an enum of endpoints (a TargetType), enforcing structure and making the network layer easy to stub for tests. Popular with teams that want a rigid, testable API definition.

    Pros
    • Enforces a structured, enum-based API layer
    • Easy stubbing for unit tests
    • Consistent conventions across a team
    • Good for large, endpoint-heavy apps
    Cons
    • Depends on Alamofire underneath (two layers)
    • The enum pattern feels heavy for small APIs
    • Less idiomatic in an async/await world
    • Steeper onboarding for newcomers
  5. 5

    Pulse

    Best debugging

    Pulse, also by Kean, is a network logger and debugger — not a request library. It records requests and responses, offers an in-app inspector, and lets you view traffic on device or in Xcode, which is invaluable for debugging APIs.

    Pros
    • In-app and Xcode network inspection
    • Works alongside URLSession, Alamofire, or Get
    • Persistent, searchable request logs
    • Huge time-saver for debugging APIs
    Cons
    • Not a networking client — complementary only
    • Adds a logging dependency
    • You should gate it out of production builds
    • Storage/config needs a little setup

URLSession vs Alamofire

URLSession (async) vs Alamofire comparison
FeatureURLSession (async)Alamofire
DependencyNone (first-party)Third-party
async/await support
Built-in retry & interceptors
Multipart uploadsManualBuilt-in
Learning curveLowMedium
Best forMost REST appsComplex networking needs

Frequently Asked Questions

Is Alamofire still worth using now that URLSession has async/await?
For many apps, no. URLSession's async methods removed most of the boilerplate that historically made Alamofire attractive. Alamofire still earns its place when you need built-in request retry, authentication-refresh interceptors, robust multipart uploads, or request validation without hand-rolling them. For straightforward REST with Codable, native URLSession is the leaner choice.
What is the difference between Get and Moya for a Swift API layer?
Get is a lightweight async/await-first typed client where you build requests directly, ideal when you want minimal ceremony. Moya models your entire API as an enum of endpoints on top of Alamofire, enforcing structure and easy stubbing for tests. Get suits lean modern apps; Moya suits teams that want a rigid, testable, convention-driven network layer.
Is Pulse a replacement for a networking library like Alamofire?
No — Pulse is a network logger and debugger, not a request client. It records and inspects the traffic your existing client produces, whether that is URLSession, Alamofire, or Get. You add Pulse alongside your networking layer to see requests and responses in-app or in Xcode, then gate it out of production builds.
How do I structure a networking layer with plain URLSession in SwiftUI?
A common pattern is a small generic async function that takes a request or endpoint description, calls URLSession's data(for:) method, checks the response status, and decodes JSON with Codable. Wrap it in a service or repository type your views and view models call. This gives you clean, testable networking with zero dependencies.
Which networking library is best for uploading images or files on iOS?
For simple uploads, native URLSession's upload tasks work fine but are somewhat verbose. If multipart uploads with progress, retries, and validation are frequent in your app, Alamofire's built-in multipart handling saves meaningful code. Pair whichever you choose with Pulse to debug the exact multipart payloads you send.

Keep exploring

Networking and APIs, solved on day one

The Swift Kit is a $99 one-time SwiftUI boilerplate with a modern networking layer, multi-provider AI, auth, and paywalls wired together. Lifetime updates, 14-day refund.

Get The Swift Kit — $99

One-time purchase · Lifetime updates · 14-day refund