Integrating a Public REST API into an iOS Application
Course: iOS App Development
Topic: Integrating a Public REST API into an iOS Application
Course Link: iOS App Development Training – JustAcademy
Demo Link: Register for a Course Demo
Website: JustAcademy Official Website
1. Introduction
A public REST API allows an application to retrieve or submit information through documented web endpoints. Many public APIs provide data such as weather forecasts, books, geographical information, products, news, or sample user records.
In iOS development, integrating a public REST API helps developers build applications that display real or sample server data instead of relying entirely on hard-coded values. Swift provides the programming language, Apple's URLSession handles network communication, Codable converts JSON into Swift models, and SwiftUI displays the resulting data.
For example, a book discovery app can request a list of books from a public API, decode the returned JSON, and display book titles and authors in a SwiftUI list.
Key concepts
- Public REST API: A web service that exposes documented endpoints accessible to external clients, sometimes with registration or API keys.
- Endpoint: A URL identifying a resource or operation.
- HTTP request: A message sent from the application to the server.
- JSON response: Structured data commonly returned by web APIs.
- URLSession: Apple's API for sending requests and receiving network data.
- Codable: Swift support for encoding and decoding data.
- SwiftUI: A declarative framework for building user interfaces.
2. Learning Objectives
After completing this topic, learners should be able to:
- Understand how public REST APIs work.
- Select an appropriate public API for an iOS project.
- Read API documentation and identify the required endpoint.
- Send GET requests using
URLSession.
- Decode JSON responses into Swift model objects.
- Display API data in SwiftUI lists and detail views.
- Handle loading indicators, empty responses, and network errors.
- Understand API keys, rate limits, and usage restrictions.
- Test public API integration in Xcode.
- Apply security, accessibility, and performance best practices.
3. What Is a Public REST API?
A public REST API is a web service designed to let external applications access specified resources through HTTP endpoints. The term public does not always mean that every request is anonymous or unrestricted. Some APIs require a developer account, an API key, authentication, or acceptance of specific usage terms.
Examples of public APIs
| API example |
Typical data |
Possible iOS project |
| Open Library API |
Book and author information |
Book discovery application |
| JSONPlaceholder |
Sample posts, users, comments, and other test records |
Learning and prototype application |
| REST Countries |
Country names, capitals, and other country information |
Country information browser |
| Public weather APIs |
Weather conditions and forecasts |
Weather dashboard |
Before choosing an API, check its official documentation, current availability, authentication requirements, response format, rate limits, and terms of use. These details may change over time.
4. Understanding the API Request-Response Cycle
When an iOS app integrates with a public API, it sends an HTTP request and processes the server's response.
- The user opens a screen or performs an action.
- The application creates a request URL.
URLSession sends the request over the network.
- The API server processes the request.
- The server returns an HTTP status code, headers, and usually a response body.
- The application validates the response.
- A JSON decoder converts the data into Swift models.
- The application updates its state, and SwiftUI refreshes the interface.
Example: A country browser requests country information from an API. The server returns a JSON array, Swift decodes the array into country objects, and SwiftUI displays each country in a list.
5. Selecting a Public API
Choosing an appropriate API is an important first step. A reliable integration begins with understanding what the service provides and what its usage rules allow.
API selection checklist
- Purpose: Does the API provide the data required by your project?
- Documentation: Are endpoint paths, parameters, and response structures clearly documented?
- HTTPS: Does the service support secure connections?
- Authentication: Does it require a key, token, or account?
- Rate limits: How many requests are allowed within a given period?
- Data format: Does the API return JSON or another format?
- Availability: Is the service suitable for the intended use and reliability requirements?
- Terms of use: Are there attribution, caching, or redistribution requirements?
For initial learning, a sample API can reduce setup complexity. For a production application, choose a service whose licensing, uptime, security, and data policies fit the project.
6. Understanding API Endpoints and HTTP Methods
An endpoint is a URL that identifies a resource or operation. The HTTP method indicates what the client intends to do.
| Method |
Purpose |
Example |
| GET |
Retrieve resources. |
Fetch a list of books. |
| POST |
Submit data, often to create a resource. |
Submit a new record. |
| PUT |
Replace a resource representation. |
Replace a profile. |
| PATCH |
Partially update a resource. |
Change a profile field. |
| DELETE |
Request removal of a resource. |
Delete a saved item. |
Public sample APIs often provide read-only or simulated operations. Do not assume that every method is supported or that a sample API permanently stores submitted changes.
7. Understanding JSON Responses
JSON (JavaScript Object Notation) is a common format for exchanging structured data. JSON supports objects, arrays, strings, numbers, Boolean values, and null.
Example JSON object
{
"id": 101,
"title": "Introduction to Swift",
"author": "Alex Morgan",
"published": true
}
Example JSON array
[
{
"id": 101,
"title": "Introduction to Swift",
"author": "Alex Morgan",
"published": true
},
{
"id": 102,
"title": "Learning SwiftUI",
"author": "Jamie Lee",
"published": true
}
]
The JSON structure determines how Swift models should be defined. An object usually maps to a Swift structure, while an array of objects can be decoded into an array of that structure.
8. Creating a Swift Model with Codable
Swift's Codable protocol makes it easier to convert JSON data into strongly typed values. A model should represent the fields returned by the selected endpoint.
import Foundation
struct Book: Codable, Identifiable {
let id: Int
let title: String
let author: String
}
This model assumes the API returns an integer id and string values for title and author. If the actual API uses different names or data types, adapt the model accordingly.
Mapping different JSON keys
Some APIs use snake_case names such as published_year. Swift code commonly uses camelCase, so CodingKeys can map the JSON key to the Swift property.
struct BookDetails: Codable {
let id: Int
let publishedYear: Int
enum CodingKeys: String, CodingKey {
case id
case publishedYear = "published_year"
}
}
When an API returns optional or nullable fields, model those fields appropriately. Required non-optional properties can cause decoding to fail if the server omits them or returns an incompatible value.
9. Sending a GET Request Using URLSession
URLSession is Apple's standard API for retrieving data from web services. The following function sends a GET request to a sample books endpoint.
import Foundation
func fetchBooks() async throws -> [Book] {
guard let url = URL(
string: "https://api.example.com/books"
) else {
throw URLError(.badURL)
}
let (data, response) = try await URLSession.shared.data(from: url)
guard let httpResponse = response as? HTTPURLResponse else {
throw URLError(.badServerResponse)
}
guard (200...299).contains(httpResponse.statusCode) else {
throw URLError(.badServerResponse)
}
return try JSONDecoder().decode([Book].self, from: data)
}
Note: api.example.com is a placeholder, not a real books API. Replace it with an endpoint that returns JSON matching the Book model.
Step-by-step explanation
- Create a valid URL for the endpoint.
- Call
URLSession.shared.data(from:) to send the request asynchronously.
- Check that the response is an
HTTPURLResponse.
- Verify that the HTTP status code indicates success.
- Decode the returned data with
JSONDecoder.
- Return the array of books to the caller.
10. Using a Real Public API
For a learning exercise, you can use JSONPlaceholder's sample posts endpoint. It is intended for testing and prototyping, and its response structure differs from the book model above.
Sample endpoint: https://jsonplaceholder.typicode.com/posts
Define the matching model
import Foundation
struct Post: Codable, Identifiable {
let id: Int
let userId: Int
let title: String
let body: String
}
Fetch posts
func fetchPosts() async throws -> [Post] {
guard let url = URL(
string: "https://jsonplaceholder.typicode.com/posts"
) else {
throw URLError(.badURL)
}
let (data, response) = try await URLSession.shared.data(from: url)
guard let httpResponse = response as? HTTPURLResponse else {
throw URLError(.badServerResponse)
}
guard (200...299).contains(httpResponse.statusCode) else {
throw URLError(.badServerResponse)
}
return try JSONDecoder().decode([Post].self, from: data)
}
This example uses a public testing endpoint. Its data is useful for learning and prototyping but should not be treated as a guaranteed production data source.
11. Creating a Reusable API Service
As applications grow, networking logic should be separated from views. A service layer centralizes requests and makes it easier to test and maintain the application.
import Foundation
enum APIError: Error {
case invalidURL
case invalidResponse
case httpStatus(Int)
}
struct PostAPIService {
func fetchPosts() async throws -> [Post] {
guard let url = URL(
string: "https://jsonplaceholder.typicode.com/posts"
) else {
throw APIError.invalidURL
}
let (data, response) = try await URLSession.shared.data(from: url)
guard let httpResponse = response as? HTTPURLResponse else {
throw APIError.invalidResponse
}
guard (200...299).contains(httpResponse.statusCode) else {
throw APIError.httpStatus(httpResponse.statusCode)
}
return try JSONDecoder().decode([Post].self, from: data)
}
}
Advantages of a service layer
- Reduces duplicated network code.
- Keeps view code focused on presentation.
- Centralizes response validation and decoding.
- Allows consistent error handling.
- Makes it easier to substitute mock data during testing.
12. Creating a ViewModel for SwiftUI
A view model manages the data and state used by the user interface. It can store API results, track loading status, and expose a user-friendly error message.
import Foundation
import Observation
@MainActor
@Observable
final class PostViewModel {
private let service = PostAPIService()
var posts: [Post] = []
var isLoading = false
var errorMessage: String?
func loadPosts() async {
isLoading = true
errorMessage = nil
defer {
isLoading = false
}
do {
posts = try await service.fetchPosts()
} catch {
errorMessage = "Unable to load posts. Please try again."
}
}
}
Explanation:
@Observable makes the model's observable state available to SwiftUI.
@MainActor ensures UI-related state changes occur on the main actor.
posts stores the retrieved records.
isLoading tracks whether a request is in progress.
errorMessage contains a message suitable for display.
defer resets the loading state when the method exits.
This approach uses the Observation framework available in modern Apple platform versions. For older deployment targets, an ObservableObject implementation with @Published properties may be more suitable.
13. Displaying Public API Data in SwiftUI
SwiftUI can present API results using List, ForEach, Text, and other view components. A loading indicator and an error message make the interface easier to understand.
import SwiftUI
struct PostListView: View {
@State private var viewModel = PostViewModel()
var body: some View {
NavigationStack {
Group {
if viewModel.isLoading && viewModel.posts.isEmpty {
ProgressView("Loading posts...")
} else if let message = viewModel.errorMessage,
viewModel.posts.isEmpty {
VStack(spacing: 12) {
Text(message)
.multilineTextAlignment(.center)
Button("Try Again") {
Task {
await viewModel.loadPosts()
}
}
}
.padding()
} else if viewModel.posts.isEmpty {
ContentUnavailableView(
"No Posts",
systemImage: "doc.text",
description: Text("No posts are available.")
)
} else {
List(viewModel.posts) { post in
VStack(alignment: .leading, spacing: 8) {
Text(post.title)
.font(.headline)
Text(post.body)
.font(.body)
.foregroundStyle(.secondary)
}
.padding(.vertical, 4)
}
}
}
.navigationTitle("Public Posts")
.task {
if viewModel.posts.isEmpty {
await viewModel.loadPosts()
}
}
.refreshable {
await viewModel.loadPosts()
}
}
}
}
This view assumes the Post, PostAPIService, and PostViewModel types defined in the earlier examples. ContentUnavailableView is available on newer Apple platform versions; for older deployment targets, create a custom empty-state view.
14. Managing Loading, Success, Empty, and Error States
API calls can take time, return no records, or fail. A good interface communicates these states instead of leaving users with a blank screen.
| State |
Meaning |
Recommended interface |
| Loading |
The request is in progress. |
Progress indicator or placeholder rows. |
| Success |
Data was received and processed. |
Display the results. |
| Empty |
The request succeeded but returned no records. |
Show an informative empty state. |
| Error |
The request or decoding failed. |
Show a friendly message and retry option. |
| Refreshing |
Existing data is being updated. |
Keep existing results visible when practical. |
For a better user experience, preserve existing data when a refresh fails, if appropriate. This prevents a temporary network issue from unnecessarily removing useful content from the screen.
15. Working with Query Parameters
Query parameters are commonly used to search, filter, sort, or paginate API results. Use URLComponents and URLQueryItem to construct the URL safely.
import Foundation
func makePostsURL(userID: Int) throws -> URL {
guard var components = URLComponents(
string: "https://jsonplaceholder.typicode.com/posts"
) else {
throw URLError(.badURL)
}
components.queryItems = [
URLQueryItem(
name: "userId",
value: String(userID)
)
]
guard let url = components.url else {
throw URLError(.badURL)
}
return url
}
The resulting URL requests posts associated with a specific user, using the query format supported by this sample API.
16. Sending POST Requests to a Public API
Some public testing APIs allow clients to submit sample data. A POST request commonly includes a JSON body and a Content-Type header.
import Foundation
struct NewPost: Codable {
let title: String
let body: String
let userId: Int
}
func createPost() async throws {
guard let url = URL(
string: "https://jsonplaceholder.typicode.com/posts"
) else {
throw URLError(.badURL)
}
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue(
"application/json; charset=UTF-8",
forHTTPHeaderField: "Content-Type"
)
request.setValue(
"application/json",
forHTTPHeaderField: "Accept"
)
let newPost = NewPost(
title: "Learning iOS Networking",
body: "Practicing public REST API integration.",
userId: 1
)
request.httpBody = try JSONEncoder().encode(newPost)
let (_, response) = try await URLSession.shared.data(for: request)
guard let httpResponse = response as? HTTPURLResponse else {
throw URLError(.badServerResponse)
}
guard (200...299).contains(httpResponse.statusCode) else {
throw APIError.httpStatus(httpResponse.statusCode)
}
}
Testing APIs may simulate create, update, and delete operations without permanently changing server data. Verify the service's documented behavior before relying on write operations.
17. Understanding HTTP Status Codes
HTTP status codes indicate how a server handled a request. Checking them is necessary because a completed network request does not automatically mean that the server operation succeeded.
| Status code |
Meaning |
Possible application behavior |
| 200 OK |
Request succeeded. |
Process the response body if expected. |
| 201 Created |
A resource was created. |
Use returned resource information when available. |
| 204 No Content |
Request succeeded without a response body. |
Do not attempt to decode a required JSON body. |
| 400 Bad Request |
The request is invalid. |
Check parameters and submitted data. |
| 401 Unauthorized |
Authentication is missing or invalid. |
Follow the API's authentication flow. |
| 403 Forbidden |
The server refuses the operation. |
Check access permissions. |
| 404 Not Found |
The endpoint or resource was not found. |
Verify the URL and resource identifier. |
| 429 Too Many Requests |
A rate limit was exceeded. |
Respect the API's retry guidance. |
| 500 Internal Server Error |
The server encountered an error. |
Show a useful message and retry only when appropriate. |
| 503 Service Unavailable |
The service is temporarily unavailable. |
Consider a controlled retry. |
18. API Keys and Authentication
Not every public API is available without authentication. Some services require an API key, while others use bearer tokens or a more complete authorization flow.
- Read the provider's authentication documentation.
- Use HTTPS when sending credentials or tokens.
- Never commit private credentials to source control.
- Do not assume that an API key embedded in a mobile application can remain secret; application binaries can be inspected.
- Use server-side mediation for sensitive secrets or privileged operations when appropriate.
- Store user-specific sensitive tokens securely, commonly using the iOS Keychain.
- Restrict credentials according to the provider's available controls.
- Never log access tokens, passwords, or other sensitive information.
Follow the provider's terms and security recommendations. A public endpoint should not be used to bypass authentication or access restrictions.
19. Rate Limits and API Usage Policies
API providers may limit the number of requests an application can make. Exceeding a limit can cause temporary failures or access restrictions.
Best practices
- Read the documented request limits.
- Avoid sending repeated requests when existing data can be reused.
- Cache responses when the provider permits it.
- Use pagination for large datasets if available.
- Debounce live-search requests.
- Respect server-provided retry instructions, including
Retry-After when present.
- Use exponential backoff for suitable transient failures.
- Do not retry indefinitely.
When an API returns HTTP 429, the application should reduce request frequency and follow the provider's documented retry policy.
20. Handling Network and Decoding Errors
API integration can fail because of connectivity problems, timeouts, server errors, malformed JSON, or unexpected data types.
| Error type |
Possible cause |
Recommended response |
| Invalid URL |
Malformed endpoint. |
Validate URL construction. |
| Connection failure |
Network unavailable or server unreachable. |
Show a helpful message and allow retry. |
| Timeout |
The response takes too long. |
Review timeout settings and retry carefully. |
| HTTP error |
The server returns a non-success status. |
Handle the status and any documented error body. |
| Decoding error |
The response does not match the model. |
Inspect the JSON and correct the model mapping. |
| Rate limit |
Too many requests. |
Slow down and respect retry guidance. |
| TLS error |
Certificate or connection security problem. |
Resolve the underlying security configuration issue. |
Example of calling the API safely
Task {
do {
let posts = try await PostAPIService().fetchPosts()
print("Retrieved \(posts.count) posts")
} catch {
print("Request failed: \(error.localizedDescription)")
}
}
In production, avoid exposing internal server details directly to users. Present clear messages and keep diagnostic information in appropriately secured logs.
21. Caching and Offline Considerations
Network access is not always reliable. Thoughtful caching and offline behavior can make an application more useful when a connection is slow or unavailable.
- Respect HTTP caching headers and the provider's caching policies.
- Cache appropriate data locally when permitted.
- Display previously loaded content when it remains useful.
- Provide a refresh action to retrieve updated information.
- Show a clear offline state when fresh data cannot be retrieved.
- Avoid storing sensitive data without a clear requirement and suitable protection.
- Keep cached data consistent with its expiration and freshness rules.
Do not assume every public API permits unrestricted storage or redistribution. Review its terms before caching data for long periods or making it available offline.
22. Accessibility and User Experience
An API-powered interface should remain understandable and usable while data is loading, unavailable, or being refreshed.
- Use meaningful labels for buttons such as Retry and Refresh.
- Support Dynamic Type and readable text sizes.
- Provide accessible descriptions for important icons.
- Ensure error messages explain what the user can do next.
- Use loading indicators without unnecessarily blocking unrelated interactions.
- Keep existing data visible during refreshes when appropriate.
- Use clear empty-state messages instead of blank screens.
- Test layouts with longer text and accessibility settings.
23. Testing Public API Integration
Testing verifies that the application handles both normal and unexpected responses correctly.
Testing checklist
- Test a valid successful response.
- Test an empty result set.
- Test an invalid endpoint.
- Test a non-success HTTP status code.
- Test malformed JSON and unexpected field types.
- Test offline and timeout conditions.
- Test rate-limit responses when possible.
- Verify that loading indicators stop after success or failure.
- Verify that the SwiftUI list updates after data is loaded.
- Use mock responses or a custom URL protocol for deterministic unit tests.
Tests should not depend unnecessarily on a live public service. Network availability, rate limits, and response data may change, so mocked responses are useful for repeatable automated tests.
24. Practical Project: Public Posts Browser
Project objective: Build a SwiftUI application that fetches sample posts from JSONPlaceholder and displays them in a list with loading and error handling.
Project requirements
- Create a
Post model conforming to Codable and Identifiable.
- Use
URLSession to request the posts endpoint.
- Validate the HTTP response and status code.
- Decode the JSON response into an array of posts.
- Create a view model to manage results and request state.
- Display post titles and bodies in a SwiftUI list.
- Add a loading indicator and an empty state.
- Add a retry button for failed requests.
- Implement pull-to-refresh.
- Test the application under different network conditions.
Suggested project structure
PublicPostsApp/
├── Models/
│ └── Post.swift
├── Services/
│ └── PostAPIService.swift
├── ViewModels/
│ └── PostViewModel.swift
├── Views/
│ └── PostListView.swift
└── PublicPostsApp.swift
Extension activities
- Add a search field for post titles.
- Filter posts by user ID.
- Create a post detail screen.
- Add pagination if supported by the chosen API.
- Persist permitted data locally for offline viewing.
- Write unit tests for the service and view model.
25. Common Interview Questions and Answers
Q1. What is a public REST API?
It is a web service that exposes documented resources or operations to external clients. It may still require registration, authentication, or an API key.
Q2. What is URLSession?
URLSession is Apple's networking API for sending HTTP requests and receiving data or transferring files.
Q3. What is Codable?
Codable combines encoding and decoding capabilities, allowing Swift types to be converted to and from supported data formats such as JSON.
Q4. Why should API responses be decoded into Swift models?
Models provide strongly typed data, improve code readability, and help catch response mismatches during decoding.
Q5. What is the purpose of a view model?
A view model manages data and state used by the interface, including loading status, results, and error messages.
Q6. Why should HTTP status codes be checked?
A request can complete but return a server error. Status-code validation helps the application distinguish successful responses from unsuccessful ones.
Q7. What is the difference between a public API and an unrestricted API?
A public API is available for external use under its rules, but it may require credentials, enforce rate limits, or restrict particular operations.
Q8. How can API keys be protected in iOS applications?
Do not treat a key embedded in a distributed app as a secret. Use provider restrictions and server-side mediation for sensitive privileged credentials where appropriate.
Q9. How should rate-limit errors be handled?
Reduce request frequency, respect retry guidance, and use controlled backoff instead of repeatedly sending requests.
Q10. How can public API integration be tested?
Test model decoding, status-code handling, loading states, and network failures using unit tests, mocked responses, or a controlled test endpoint.
26. Quick Revision Checklist
- A public REST API exposes documented endpoints for external applications.
- Check the endpoint, response format, authentication, and usage policies before integration.
- Use HTTPS for secure network communication.
- Use
URLSession to perform network requests.
- Use
Codable to decode JSON into Swift models.
- Validate the HTTP response and status code.
- Separate network logic into a service layer.
- Use a view model to manage data and UI state.
- Show loading, success, empty, and error states.
- Respect rate limits and retry instructions.
- Follow authentication, caching, and attribution requirements.
- Test successful responses and failure scenarios.
27. Conclusion
Integrating a public REST API into an iOS application allows developers to build dynamic applications that retrieve data from remote services. By using URLSession, Codable, Swift concurrency, a reusable API service, and SwiftUI state management, developers can fetch JSON, convert it into Swift models, and present the information in a responsive interface.
A reliable implementation should also validate HTTP status codes, handle network and decoding errors, respect authentication and rate limits, and follow the API provider's terms. Practicing with a sample posts API is an effective way to build the foundation for more advanced iOS networking projects.
Explore the iOS App Development Course: View iOS Training at JustAcademy
Register for a Course Demo: Book a Course Demo
Official Website: Visit JustAcademy