Swift
GET request
HTTP request
URLSession
networking

Swift GET request with parameters

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design

Introduction

In Swift, a GET request with parameters should usually be built with URLComponents and URLQueryItem. That approach is safer than concatenating query strings by hand because encoding is handled automatically and the resulting URL is easier to inspect and test.

Building the URL correctly

Suppose an endpoint accepts a search term, a page number, and a sort option. The safest construction is:

swift
1import Foundation
2
3var components = URLComponents(string: "https://api.example.com/products")!
4components.queryItems = [
5    URLQueryItem(name: "q", value: "wireless headphones"),
6    URLQueryItem(name: "page", value: "1"),
7    URLQueryItem(name: "sort", value: "price_asc")
8]
9
10guard let url = components.url else {
11    fatalError("Invalid URL")
12}
13
14print(url.absoluteString)

This matters because spaces, ampersands, and other reserved characters must be percent-encoded correctly. URLComponents does that work for you.

Sending the request with URLSession

Once the URL is built, you can fetch data with Swift concurrency:

swift
1import Foundation
2
3struct Product: Decodable {
4    let id: Int
5    let name: String
6}
7
8struct ProductResponse: Decodable {
9    let items: [Product]
10}
11
12func fetchProducts() async throws -> [Product] {
13    var components = URLComponents(string: "https://api.example.com/products")!
14    components.queryItems = [
15        URLQueryItem(name: "q", value: "wireless headphones"),
16        URLQueryItem(name: "page", value: "1")
17    ]
18
19    let url = try unwrapURL(from: components)
20    let (data, response) = try await URLSession.shared.data(from: url)
21
22    guard let http = response as? HTTPURLResponse, 200...299 ~= http.statusCode else {
23        throw URLError(.badServerResponse)
24    }
25
26    let decoded = try JSONDecoder().decode(ProductResponse.self, from: data)
27    return decoded.items
28}
29
30func unwrapURL(from components: URLComponents) throws -> URL {
31    guard let url = components.url else {
32        throw URLError(.badURL)
33    }
34    return url
35}

The request stays readable, and the decoding logic is separate from the URL assembly.

Adding headers and request options

Even for a GET request, you may need custom headers such as Accept or authentication:

swift
1import Foundation
2
3func fetchWithHeaders(url: URL) async throws -> Data {
4    var request = URLRequest(url: url)
5    request.httpMethod = "GET"
6    request.timeoutInterval = 15
7    request.setValue("application/json", forHTTPHeaderField: "Accept")
8    request.setValue("Bearer token-value", forHTTPHeaderField: "Authorization")
9
10    let (data, response) = try await URLSession.shared.data(for: request)
11
12    guard let http = response as? HTTPURLResponse, 200...299 ~= http.statusCode else {
13        throw URLError(.badServerResponse)
14    }
15
16    return data
17}

That pattern scales better than calling data(from:) once request customization becomes necessary.

Reusing query-building logic

If your app calls several endpoints, centralize the construction logic in a small client:

swift
1import Foundation
2
3final class APIClient {
4    let baseURL = URL(string: "https://api.example.com")!
5
6    func makeURL(path: String, query: [String: String]) throws -> URL {
7        var components = URLComponents(
8            url: baseURL.appendingPathComponent(path),
9            resolvingAgainstBaseURL: false
10        )!
11
12        components.queryItems = query.map { URLQueryItem(name: $0.key, value: $0.value) }
13
14        guard let url = components.url else {
15            throw URLError(.badURL)
16        }
17
18        return url
19    }
20}

This keeps callers focused on endpoint intent instead of repetitive encoding logic.

Common Pitfalls

The biggest mistake is manual string concatenation such as "https://... ?q=" + term. That tends to break when values contain spaces, slashes, ampersands, or non-ASCII text.

Another issue is treating all non-200 responses the same without checking the actual status code range or server payload. Real APIs often return meaningful error bodies.

People also mix URL construction, networking, and JSON decoding into one long method. That works for one request and becomes difficult to test after the third or fourth endpoint.

Finally, remember that GET parameters belong in the URL query, not in the HTTP body. Some servers ignore GET bodies entirely.

Summary

  • Use URLComponents and URLQueryItem to build GET requests with parameters in Swift.
  • Use URLSession to send the request and validate the HTTP status code explicitly.
  • Switch to URLRequest when you need headers, timeouts, or other request customization.
  • Keep URL-building logic reusable instead of hand-writing query strings everywhere.
  • Let the standard library handle encoding instead of doing it manually.

Related reading
Course
Beginner
27 lessons
10 hours
System Design Fundamentals

Build a strong foundation in designing scalable, reliable distributed systems.

View the course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design

All Rights Reserved.