How to Make an HTTP Request in Swift with URLSession

How to Make an HTTP Request in Swift with URLSession

Written by

in

Quick answer: Use Foundation’s URLSession. For a GET, create a URL and call URLSession.shared.data(from:) from an async context, then check the HTTPURLResponse status code and decode the Data. For POST, build a URLRequest, set the method and body, and call data(for:).

Why this happens

Swift itself has no built-in networking API, and there is no curl equivalent in the language. The networking types live in Foundation, which is available on iOS, macOS, watchOS, and tvOS without importing Objective-C headers by hand. URLSession is the modern API; NSURLConnection is the old one and is deprecated.

The confusion usually comes from older tutorials that mix Swift 1.x or 2.x syntax (NSURLConnection, NSURLRequest, NSJSONSerialization) with current Swift. In current Swift the types are URL, URLRequest, URLSession, and JSONDecoder.

A request can also look correct and still fail because App Transport Security blocks plain http:// URLs, or because the completion handler runs off the main thread and the UI update is ignored.

How to fix it

1. Make a simple GET request with URLSession

Create a URL, create a data task, and call resume(). The task does nothing until you resume it.

import Foundation

let url = URL(string: "https://jsonplaceholder.typicode.com/posts/1")!

let task = URLSession.shared.dataTask(with: url) { data, response, error in
    if let error {
        print("Request failed: \(error.localizedDescription)")
        return
    }
    guard let data else { return }
    print(String(decoding: data, as: UTF8.self))
}

task.resume()

dataTask(with:) is asynchronous. If you forget resume(), nothing happens. The closure is not guaranteed to run on the main thread, so do not update UIKit or SwiftUI state directly inside it.

For a GET with headers, use a URLRequest instead of a bare URL:

var request = URLRequest(url: url)
request.setValue("Bearer YOUR_TOKEN", forHTTPHeaderField: "Authorization")

let task = URLSession.shared.dataTask(with: request) { data, response, error in
    // Handle the response here.
}
task.resume()

2. Prefer async/await for new code

URLSession has async methods. Use data(from:) for a GET and data(for:) for a configured request.

func fetchData(from url: URL) async throws -> Data {
    let (data, response) = try await URLSession.shared.data(from: url)

    guard let http = response as? HTTPURLResponse else {
        throw URLError(.badServerResponse)
    }
    guard (200..<300).contains(http.statusCode) else {
        throw URLError(.badServerResponse)
    }
    return data
}

Call it from a Task or a SwiftUI .task modifier. Mark the calling function @MainActor if it updates UI state.

@MainActor
func load() async {
    do {
        let data = try await fetchData(from: url)
        items = try JSONDecoder().decode([Item].self, from: data)
    } catch {
        errorMessage = error.localizedDescription
    }
}

3. Send a POST request with a JSON body

Build a URLRequest, set the HTTP method, set the Content-Type header, and encode the body.

struct NewPost: Encodable {
    let title: String
    let body: String
    let userId: Int
}

func createPost(title: String, body: String) async throws -> Data {
    let url = URL(string: "https://jsonplaceholder.typicode.com/posts")!
    var request = URLRequest(url: url)
    request.httpMethod = "POST"
    request.setValue("application/json", forHTTPHeaderField: "Content-Type")
    request.httpBody = try JSONEncoder().encode(
        NewPost(title: title, body: body, userId: 1)
    )

    let (data, response) = try await URLSession.shared.data(for: request)

    guard let http = response as? HTTPURLResponse,
          (200..<300).contains(http.statusCode) else {
        throw URLError(.badServerResponse)
    }
    return data
}

For form posts, set application/x-www-form-urlencoded and percent-encode the body. For file uploads, use upload(for:from:).

4. Decode the response and check the status code

URLSession does not throw for HTTP 404 or 500. It only throws for transport errors. Check HTTPURLResponse.statusCode yourself.

struct Post: Decodable {
    let id: Int
    let title: String
    let body: String
}

func fetchPost() async throws -> Post {
    let url = URL(string: "https://jsonplaceholder.typicode.com/posts/1")!
    let data = try await fetchData(from: url)
    return try JSONDecoder().decode(Post.self, from: data)
}

If decoding fails, print the raw string first: print(String(decoding: data, as: UTF8.self)). That shows whether the server returned JSON, HTML, or an error page. For APIs that return dates as strings, configure JSONDecoder.dateDecodingStrategy instead of decoding into String and parsing later.

5. Test in an Xcode playground

Asynchronous code in a playground stops when the playground reaches the end. Add PlaygroundSupport and keep the playground running.

import PlaygroundSupport
import Foundation

PlaygroundPage.current.needsIndefiniteExecution = true

let url = URL(string: "https://jsonplaceholder.typicode.com/posts/1")!
let task = URLSession.shared.dataTask(with: url) { data, _, _ in
    if let data {
        print(String(decoding: data, as: UTF8.self))
    }
    PlaygroundPage.current.finishExecution()
}
task.resume()

Without needsIndefiniteExecution, the task may be cancelled before the response arrives.

How to prevent it

  • Use https:// URLs. Plain http:// is blocked by App Transport Security unless you add an exception, and broad exceptions are a bad habit.
  • Check HTTPURLResponse.statusCode; do not treat every response as success.
  • Handle the error parameter or catch the thrown error. Do not ignore it.
  • Avoid force-unwrapping URL(string:) for URLs that come from user input or a server.
  • Keep UI updates on the main actor; networking callbacks run on a background queue.
  • Cancel tasks you no longer need, especially in view controllers or SwiftUI views that can disappear.
  • If the default timeout is wrong for your API, set timeoutIntervalForRequest on a custom URLSessionConfiguration instead of blocking the main thread.

FAQ

Do I need to import Objective-C classes to make an HTTP request in Swift?

No. import Foundation gives you URL, URLRequest, URLSession, and JSONDecoder. You do not need to bridge to Objective-C or add a third-party library for basic requests.

Why does my request fail with an App Transport Security error?

You are probably requesting a plain http:// URL. iOS blocks cleartext HTTP by default. Use HTTPS, or add a narrow ATS exception in Info.plist only for a server you control. The usual message is App Transport Security has blocked a cleartext HTTP (http://) resource load since it is insecure.

Can I still use NSURLConnection?

It is deprecated and should not be used for new code. Use URLSession instead. If you maintain old code, migrate the request to URLSession.dataTask(with:completionHandler:) or the async data(for:) method.

Sources & credits

Parts of this guide are adapted from content licensed under CC BY-SA 4.0; this article is shared under the same license.