Working with APIs
By the end of this lesson you'll fetch real data from the internet with URLSession , turn JSON into Swift types with Codable , handle failures cleanly with do/try/catch , and show the result in a SwiftUI view — the skill that turns a static app into a live one.
Learn Working with APIs in our free Swift course — a beginner-friendly interactive lesson with worked examples, a practice exercise and a quick reference.
Part of the free Swift course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
What You'll Learn in This Lesson
1️⃣ Codable — Turning JSON Into Swift Types
APIs speak JSON — plain text like { "id": 1, "title": "Hello" } . To use it, you describe its shape with a model (a struct ) and mark it Codable . That one word lets JSONDecoder read the text and fill in your struct automatically — every property name must match a JSON key, with the matching type. Read this worked example and run it; it needs no network.
2️⃣ URLSession + async/await — Fetching Over the Network
URLSession is Swift's built-in networking tool. The modern way to use it is async/await : you write try await URLSession.shared.data(from: url) and the line pauses until the bytes arrive, then carries on — reading top-to-bottom like ordinary code, with no nested callbacks. URL(string:) returns an Optional, so unwrap it safely, and remember URLSession only throws for transport errors — you check the HTTP status yourself.
Your turn. Below, the JSON and the calls are written for you — fill in the four ___ blanks marked with // 👉 to make the model and the decode work.
3️⃣ Error Handling — do / try / catch
Networking fails all the time — no signal, a typo in a URL, a server hiccup. Swift makes that explicit: a function that can fail is marked throws , you call it with try , and you wrap the call in do { … } catch { … } so a failure becomes a message instead of a crash. Inside catch , the thing that went wrong is available as error .
Now you try. This function is missing the three concurrency/error keywords. Fill in the three ___ blanks so it suspends, fetches, and handles failure.
4️⃣ Using the Data in SwiftUI with .task
A view that shows live data stores it in @State and loads it when it appears. The .task { … } modifier runs async code as the view shows up and cancels it automatically if the view disappears — so prefer it over .onAppear for loading. Code inside .task already runs on the main actor, so assigning to @State there updates the UI safely.
Deep Dive: parallel requests with async let
Sequential await calls wait for each other. When requests are independent, async let starts them all at once so the screen loads faster:
For snake_case APIs, skip hand-written CodingKeys with one line: decoder.keyDecodingStrategy = .convertFromSnakeCase maps first_name to firstName for the whole response.
Common Errors (and the fix)
- "Errors thrown from here are not handled" / "'async' call in a function that does not support concurrency": you forgot try or await . A networking call needs both: try await URLSession.shared.data(from: url) , inside a function marked async throws (or a Task { } ).
- "keyNotFound" / "typeMismatch" when decoding: your struct doesn't match the JSON. Fix the property name/type, make a missing field Optional with ? , or add CodingKeys to map a different JSON key (e.g. case userId = "user_id" ).
- "Publishing changes from background threads is not allowed" / purple runtime warning: you updated the UI off the main thread. Update @State inside .task (already main-actor), or wrap the UI work in await MainActor.run { … } .
- App crashes with "Unexpectedly found nil" on the URL: you force-unwrapped URL(string: "…")! on a malformed string. Unwrap safely with guard let url = URL(string: …) else { throw … } instead.
- Request "succeeds" but you got an error page: URLSession doesn't throw on 404/500. Cast to HTTPURLResponse and check statusCode == 200 , throwing your own error otherwise.
📋 Quick Reference — Networking & JSON
Task
Swift Code
Build a URL safely
guard let url = URL(string: s) else { ... }
GET request
try await URLSession.shared.data(from: url)
Decode JSON
JSONDecoder().decode(T.self, from: data)
Encode JSON
JSONEncoder().encode(value)
snake_case keys
decoder.keyDecodingStrategy = .convertFromSnakeCase
Check status
(response as? HTTPURLResponse)?.statusCode
Handle errors
do { try await ... } catch { ... }
Load in SwiftUI
.task { await loadData() }
Frequently Asked Questions
Mini-Challenge: Fetch a To-Do
No blanks this time — just a brief and an outline. Put together everything from this lesson: a Codable model, an async throws fetch, and a do/catch . Build it in Xcode and check your output against the expected line in the comments.
🎉 Course Complete — You Did It!
This was the final Swift lesson. Here's what you can now do:
- ✅ Model JSON with Codable structs and decode it with JSONDecoder
- ✅ Fetch data over the network with URLSession and async/await
- ✅ Build URLs safely and check HTTP status codes
- ✅ Handle failures with do / try / catch and custom error types
- ✅ Show live data in SwiftUI with @State and .task
Where to go next: wire this into a real app idea. Persist data with SwiftData or Core Data, add pull-to-refresh and loading states, structure logic into an MVVM view model, then publish to TestFlight . Practise by building a weather, news, or GitHub-search app against a free public API — you now have every piece you need.
Practice quiz
Which protocol lets a struct map to and from JSON?
- Codable
- Serializable
- JSONMappable
- Decoder
Answer: Codable. Conforming to Codable enables automatic JSON encoding and decoding.
Which type reads JSON bytes into a Swift value?
- JSONReader
- JSONDecoder
- JSONParser
- Decoder.shared
Answer: JSONDecoder. JSONDecoder().decode(...) turns Data into your model.
Which keyword marks a function that can pause for async work?
- throws
- defer
- async
- lazy
Answer: async. async marks a function that can suspend while awaiting results.
What does await do in a networking call?
- Throws an error
- Declares a constant
- Retries the request
- Suspends until the result arrives
Answer: Suspends until the result arrives. await pauses the function until the async result is ready.
Does URLSession throw an error on a 404 response?
- No, you must check the status code yourself
- Yes, always
- Only on 500
- Only without internet
Answer: No, you must check the status code yourself. URLSession only throws for transport errors; check statusCode == 200 yourself.
Which construct handles a thrown error safely?
- if let / else
- do / try / catch
- guard / return
- switch / default
Answer: do / try / catch. do { try ... } catch { ... } catches errors instead of crashing.
What does URL(string:) return?
- A String
- A Data value
- An Optional URL
- An error
Answer: An Optional URL. URL(string:) is failable and returns an optional, so unwrap it safely.
Which keyword marks a call inside catch-able code that may fail?
- catch
- async
- await
- try
Answer: try. try marks the exact call that may throw an error.
Which SwiftUI modifier runs async loading when a view appears?
- .task
- .onTap
- .refreshable
- .async
Answer: .task. .task runs async code on appear and auto-cancels on disappear.
What do you pass to decode to name the target type?
- Post()
- Post
- Post.self
- "Post"
Answer: Post.self. Pass the type itself, e.g. Post.self, to JSONDecoder().decode.
Continue this course
- Previous: SwiftUI Basics
- Next: Optionals & nil Safety