The URLSession API of the Foundation framework is a modern, first-party API to perform HTTP requests in Swift. There is no need for third-party libraries, such as Alamofire, if you need to make a few HTTP requests to an API. Foundation's URLSession API offers everything you need.

In a previous tutorial, I wrote about making a GET request using the URLSession API. In this tutorial, you learn how to make a POST request, another common operation in client apps.

In this tutorial, we take a look at three APIs, (1) a completion handler, (2) Swift concurrency, and (3) a reactive approach using the Combine framework.

Creating a Playground

Fire up Xcode and create a playground by selecting New > Playground... from Xcode's File menu. Choose the Blank template from the iOS > Playground section.

How to Make a POST Request in Swift

Remove the contents of the playground with the exception of the import statement for the UIKit framework at the top. The URLSession class is defined in the Foundation framework. Importing UIKit automatically imports Foundation.

import UIKit

Working with URLSession

If you want to learn more about URLSession, then I recommend reading How to Make an HTTP Request in Swift. In that tutorial, we take a closer look at the URLSession API. In this tutorial, we focus on the steps you need to take to make a PUT or a POST request using the URLSession API.

The goal of this tutorial is to send a JSON object to a web service. If we want to send data to a web service, we need to make a PUT or a POST request. In other words, the HTTP verb is PUT or POST. We make a POST request in this tutorial, but the only difference with a PUT request is the HTTP method. Don't worry if this sounds confusing. It becomes clear in a few moments.

Option 1: Completion Handler

We first define the URL for the endpoint of the web service we send the request to. I set up a mock web service for this tutorial. The URL of the mock web service is https://cocoacasts.com/mockapi. The endpoint we use in this tutorial is the message endpoint, so the URL for the POST request is https://cocoacasts.com/mockapi/message. Note that we force unwrap the URL object the initializer returns. That is fine for this tutorial.

import UIKit

let url = URL(string: "https://cocoacasts.com/mockapi/message")!

The next step is creating a URLRequest object. We create a mutable request because we need to configure the request before we send it.

import UIKit

let url = URL(string: "https://cocoacasts.com/mockapi/message")!

var request = URLRequest(url: url)

The first property we set is the httpMethod property. To send a POST request to the mock web service, we set the httpMethod property of the URLRequest object to POST.

import UIKit

let url = URL(string: "https://cocoacasts.com/mockapi/message")!

var request = URLRequest(url: url)
request.httpMethod = "POST"

The message endpoint expects a JSON object with three fields, userID, toUserID, and message. The value of userID is the ID of the sender of the message, the value of toUserID is the ID of the recipient of the message, and the value of message is the message that is sent. The message endpoint returns a 200 response on success with no content.

We first create a struct with name Message that defines the body or payload of the request. We conform the struct to the Encodable protocol to convert an instance of the struct to a Data object. Why that is necessary becomes clear in a few moments.

struct Message: Encodable {

    let userID: Int
    let toUserID: Int
    let message: String

}

We create a Message object and use a JSONEncoder instance to convert it to a Data object. We cannot send the Message object as is. It needs to be converted to a Data object. That is why we conformed the Message struct to the Encodable protocol.

let message = Message(
    userID: 123,
    toUserID: 456,
    message: "Lorem ipsum dolor sit amet, consectetur adipiscing elit."
)

let data = try! JSONEncoder().encode(message)

Note that we use the try! operator to avoid having to deal with optionals in the playground. In production, you would handle any errors that could be thrown to fail gracefully.

With the Data object created, we can attach it to the request by setting its httpBody property.

request.httpBody = data

We need to make it clear to the mock web service that we are sending a JSON object. That is typically done by setting the Content-Type header of the request. We invoke the setValue(_:forHTTPHeaderField:) method on the URLRequest object, passing in application/json as the value and Content-Type as the name of the header.

request.setValue(
    "application/json", 
    forHTTPHeaderField: "Content-Type"
)

To send the request to the mock web service, we ask the shared URLSession instance to create a data task by invoking the dataTask(with:completionHandler:) method. It returns a URLSessionDataTask instance and accepts two arguments, a URLRequest object and a completion handler. The completion handler, a closure, is executed when the data task completes, successfully or unsuccessfully. The completion handler accepts three arguments, an optional Data object, an optional URLResponse object, and an optional Error object. The implementation is similar to the one in How to Make an HTTP Request in Swift. The only difference is that we pass a URLRequest object to the dataTask(with:completionHandler:) method instead of a URL object.

let task = URLSession.shared.dataTask(with: request) { data, response, error in
	
}

We are only interested in the URLResponse object. To know if the request was successful, we inspect the status code of the response. This is a bit clunky because the response is of type URLResponse. We cast the response to HTTPURLResponse and read the value of its statusCode property. Note that we use the as! operator to cast the response to HTTPURLResponse. The cast should never fail. The request is successful if the status code is equal to 200.

let task = URLSession.shared.dataTask(with: request) { data, response, error in
    let statusCode = (response as! HTTPURLResponse).statusCode

    if statusCode == 200 {
        print("SUCCESS")
    } else {
        print("FAILURE")
    }
}

Remember from How to Make an HTTP Request in Swift that we need to call resume() on the URLSessionDataTask instance to execute it.

let task = URLSession.shared.dataTask(with: request) { data, response, error in
    let statusCode = (response as! HTTPURLResponse).statusCode

    if statusCode == 200 {
        print("SUCCESS")
    } else {
        print("FAILURE")
    }
}

task.resume()

Run the contents of the playground and inspect the output in the console. You should see SUCCESS printed to the console.

SUCCESS

Option 2: Swift Concurrency

The completion handler we passed to the dataTask(with:completionHandler:) method is invoked when the request completes, successfully or unsuccessfully. That works fine, but there is a more elegant and modern API that leverages Swift concurrency.

Sending the POST request is an asynchronous operation and we do that in a Task. In the body of the Task, we invoke the data(for:) method on the shared URLSession instance. The data(for:) method accepts a URLRequest object. It is a throwing method, so we wrap it in a do-catch statement. We prefix the method call with the await keyword because the method is asynchronous.

Task {
    do {
        let (_, response) = try await URLSession.shared.data(for: request)
    } catch {
        print("Failed to Send POST Request \(error)")
    }
}

The method returns a tuple with two values, a Data object and a URLResponse object. We are not interested in the Data object. As before, we use the URLResponse object to figure out whether the POST request was successful.

Task {
    do {
        let (_, response) = try await URLSession.shared.data(for: request)

        let statusCode = (response as! HTTPURLResponse).statusCode

        if statusCode == 200 {
            print("SUCCESS")
        } else {
            print("FAILURE")
        }
    } catch {
        print("Failed to Send POST Request \(error)")
    }
}

The beauty of using Swift concurrency is that we can read this snippet of code from top to bottom. That isn't true if we use a completion handler. Another major benefit is how errors are handled. The method is throwing, which forces us to be conscious of any errors that may be thrown if something goes wrong. What I like most, however, is the fact that the Data and URLResponse objects of the tuple are not optionals. If the POST request succeeds, then we are guaranteed to have access to a Data object and a URLResponse object. Remember that the Data, URLResponse, and Error objects of the completion handler are optionals. That isn't ideal for several reasons.

Option 3: Reactive Programming with Combine

There is one last option I would like to show you. I am a big fan of reactive programming and the Combine framework is Apple's implementation of a reactive framework for Apple development. We can ask the shared URLSession instance for a publisher that emits the response of the POST request or, if the request fails, terminates with an error.

URLSession.shared.dataTaskPublisher(for: request)

We invoke the sink(receiveCompletion:receiveValue:) method to subscribe to the publisher. The completion handler, the first argument of the sink(receiveCompletion:receiveValue:) method, is invoked when the request completes, successfully or unsuccessfully. We can get access to the Data and URLResponse objects through the value handler, the second argument of the sink(receiveCompletion:receiveValue:) method.

let cancellable = URLSession.shared.dataTaskPublisher(for: request)
    .sink(receiveCompletion: { completion in
        switch completion {
        case .finished:
            ()
        case .failure(let error):
            print("Failed to Send POST Request \(error)")
        }
    }, receiveValue: { _, response in
        let statusCode = (response as! HTTPURLResponse).statusCode

        if statusCode == 200 {
            print("SUCCESS")
        } else {
            print("FAILURE")
        }
    })

The sink(receiveCompletion:receiveValue:) method returns an AnyCancellable. We need to hold on to the AnyCancellable until the request completes. For that reason, we store a reference to the AnyCancellable in a constant with name cancellable.

What's Next?

In this tutorial, we explored three URLSession APIs to send a POST request to a web service. While each API has its pros and cons, the async data(for:) method and the reactive dataTaskPublisher(for:) method are the preferred and more modern solutions. My recommendation is to use the async data(for:) method unless you need a reactive solution. That said, nothing stops you from using the API that uses a completion handler.