Simple REST API Calls With Swift
February 03, 2015 - Updated: November 10, 2016 - Swift 3.0

Pretty much every app these days consumes or creates content through an API. In this tutorial we’ll use Alamofire, a rich networking library, to interact with web services but you can also use iOS’s URLSession to make REST calls.

This tutorial has been updated for Swift 3.0 and iOS 10.

REST API Calls with URLSession

The function to make an async URL request is part of URLSession:


open func dataTask(with request: URLRequest, 
  completionHandler: @escaping (Data?, URLResponse?, Error?) -> Swift.Void
  ) -> URLSessionDataTask

It takes a request which contains the URL then goes off and sends the request. Once it gets a response (or has an error to report), the completion handler gets called. The completion handler is where we can work with the results of the call: error checking, saving the data locally, updating the UI, whatever.

The simplest case is a GET request. Of course, we need an API to hit. Fortunately there’s super handy JSONPlaceholder:

“JSONPlaceholder is a fake online REST API for testing and prototyping. It’s like image placeholders but for web developers.”

JSONPlaceholder has a handful of resources similar to what you’ll find in a lot of apps: users, posts, photos, albums, … We’ll stick with Todos.

First let’s print out the title of the first todo (assuming that there are todos, which this dummy API already has). To get a single todo, we need to make a GET call to the todos endpoint with an ID number. Checking out https://jsonplaceholder.typicode.com/todos/ we can see that the id for the first todo is 1. So let’s grab it:

First, set up the URL request:

let todoEndpoint: String = "https://jsonplaceholder.typicode.com/todos/1"
guard let url = URL(string: todoEndpoint) else {
  print("Error: cannot create URL")
  return
}
let urlRequest = URLRequest(url: url)

The guard statement lets us check that the URL we’ve provided is valid.

Then we need a URLSession to use to send the request, we can use the default shared session:

let session = URLSession.shared

Then create the data task:

let task = session.dataTask(with: urlRequest, completionHandler:{ _, _, _ in })

{ _, _, _ in } looks funny but it’s just an empty completion handler. Since we just want to execute our request and not deal with the results yet, we can specify an empty completion handler here. It has to have input arguments to match the type of completion handler that’s expected, hence the _, _, _ placeholders for those three arguments.

And finally send it (yes, this is an oddly named function):

task.resume()

Calling this now will hit the URL (from the urlRequest) and obtain the results (using a GET request since that’s the default). To actually get the results to do anything useful we need to implement the completion handler.

Completion handlers are super convenient when your app is doing something that might take a little while, like making an API call, and you need to do something when that task is done, like updating the UI to show the data.

Here’s where the code to handle the response from the API will go:


let task = session.dataTask(with: urlRequest) { data, response, error in
  // do stuff with response, data & error here
}
task.resume()

Now we have access to three arguments: the data returned by the request, the URL response, and an error (if one occurred). So let’s check for errors and figure out how to get at the data that we want: the first todo’s title. We need to:

  1. Make sure we got data and no error
  2. Try to transform the data into JSON (since that’s the format returned by the API)
  3. Access the todo object in the JSON and print out the title

let task = session.dataTask(with: urlRequest) {
  (data, response, error) in
  // check for any errors
  guard error == nil else {
    print("error calling GET on /todos/1")
    print(error!)
    return
  }
  // make sure we got data
  guard let responseData = data else {
    print("Error: did not receive data")
    return
  }
  // parse the result as JSON, since that's what the API provides
  do {
    guard let todo = try JSONSerialization.jsonObject(with: responseData, options: [])
      as? [String: Any] else {
      print("error trying to convert data to JSON")
      return
    }
    // now we have the todo
    // let's just print it to prove we can access it
    print("The todo is: " + todo.description)
    
    // the todo object is a dictionary
    // so we just access the title using the "title" key
    // so check for a title and print it if we have one
    guard let todoTitle = todo["title"] as? String else {
      print("Could not get todo title from JSON")
      return
    }
    print("The title is: " + todoTitle)
  } catch  {
    print("error trying to convert data to JSON")
    return
  }
}
task.resume()

Which prints out:


The todo is: {
    completed = 0;
    id = 1;
    title = "delectus aut autem";
    userId = 1;
}
The title is: delectus aut autem

It’s a little verbose but if you just need a quick GET call to an API without authentication, that’ll do it.

If you need a HTTP method type other than GET then you can set the HTTP method in the URLRequest so you can set the method type. Since we’ll have to do that after the URLRequest is created, we need to declare it with var, not let. Set the code you’ve been working on aside (you can just comment it out) so we can create a POST request instead:


let todosEndpoint: String = "https://jsonplaceholder.typicode.com/todos"
guard let todosURL = URL(string: todosEndpoint) else {
  print("Error: cannot create URL")
  return
}
var todosUrlRequest = URLRequest(url: todosURL)
todosUrlRequest.httpMethod = "POST"

Then we can set the new todo as the httpBody for the request:


let newTodo: [String: Any] = ["title": "My First todo", "completed": false, "userId": 1]
let jsonTodo: Data
do {
  jsonTodo = try JSONSerialization.data(withJSONObject: newTodo, options: [])
  todosUrlRequest.httpBody = jsonTodo
} catch {
  print("Error: cannot create JSON from todo")
  return
}

JSONSerialization.data(withJSONObject: options:) converts JSON that we’ve created as dictionaries and arrays into data so we can send it as part of the urlRequest.

Now we can execute the request:


let session = URLSession.shared
let task = session.dataTask(with: todosUrlRequest) { _, _, _ in }
task.resume()

If it’s working correctly then we should get our todo back as a response along with the id number assigned to it. Since it’s just for testing, JSONPlaceholder will let you do all sorts of REST requests (GET, POST, PUT, PATCH, DELETE and OPTIONS) but it won’t actually change the data based on your requests. So when we send this POST request, we’ll get a response with an ID to confirm that we did it right but it won’t actually be kept in the database so we can’t access it on subsequent calls. We can use the same error checking and parsing that we used with our GET request to make sure the API call worked:


let todosEndpoint: String = "https://jsonplaceholder.typicode.com/todos"
guard let todosURL = URL(string: todosEndpoint) else {
  print("Error: cannot create URL")
  return
}
var todosUrlRequest = URLRequest(url: todosURL)
todosUrlRequest.httpMethod = "POST"
let newTodo: [String: Any] = ["title": "My First todo", "completed": false, "userId": 1]
let jsonTodo: Data
do {
  jsonTodo = try JSONSerialization.data(withJSONObject: newTodo, options: [])
  todosUrlRequest.httpBody = jsonTodo
} catch {
  print("Error: cannot create JSON from todo")
  return
}

let session = URLSession.shared

let task = session.dataTask(with: todosUrlRequest) {
  (data, response, error) in
  guard error == nil else {
    print("error calling POST on /todos/1")
    print(error)
    return
  }
  guard let responseData = data else {
    print("Error: did not receive data")
    return
  }
  
  // parse the result as JSON, since that's what the API provides
  do {
    guard let receivedTodo = try JSONSerialization.jsonObject(with: responseData,
      options: []) as? [String: Any] else {
        print("Could not get JSON from responseData as dictionary")
        return
    }
    print("The todo is: " + receivedTodo.description)
    
    guard let todoID = receivedTodo["id"] as? Int else {
      print("Could not get todoID as int from JSON")
      return
    }
    print("The ID is: \(todoID)")
  } catch  {
    print("error parsing response from POST on /todos")
    return
  }
}
task.resume()

Deleting is pretty similar (minus creating the JSON todo):


let firstTodoEndpoint: String = "https://jsonplaceholder.typicode.com/todos/1"
var firstTodoUrlRequest = URLRequest(url: URL(string: firstTodoEndpoint)!)
firstTodoUrlRequest.httpMethod = "DELETE"

let session = URLSession.shared

let task = session.dataTask(with: firstTodoUrlRequest) {
  (data, response, error) in
  guard let _ = data else {
    print("error calling DELETE on /todos/1")
    return
  }
  print("DELETE ok")
}
task.resume()

So that’s how to call a REST API from Swift using URLSession. There are a couple of gotchas though:

We’re assuming we’ll get results and they’ll be in the format we expect. We’ve got error handling to make sure the todo is valid JSON:


do {
  guard let receivedTodo = try JSONSerialization.jsonObject(with: responseData,
    options: []) as? [String: Any] else {
      // ...
      return
  }
  // handle valid todo
} catch  {
  print("error parsing response from POST on /todos")
  return
}

That catch statement will catch anything that isn’t valid JSON. In other words, any time JSONSerialization.jsonObject(with: , options: ) can’t convert the responseData into a valid Any.

Since the JSON parsing gives us an Any we then need to check that the JSON is a dictionary using:


as? [String: Any]

The guard’s else statement will get called if the JSON is valid but it isn’t a [String: Any] dictionary. That can happen if the JSON has an array at the top level. We’ve checked that this API call should return a dictionary so getting an array instead is something we’d consider an error:


guard let receivedTodo = try JSONSerialization.jsonObject(with: responseData,
  options: []) as? [String: Any] else {
  // Got valid JSON but it isn't [String: Any]
  print("JSON isn't a [String: Any]")
  return
}
// Here we have valid JSON and it's a [String: Any]

So that’s the quick & dirty way to call a REST API from Swift.

Grab the code on GitHub: REST gists