Photo by Blake Connally / Unsplash

Integrating Swift with Backend Service

Coding May 5, 2023

As the Swift programming language has evolved over the years there have been many different approaches to communicating with a backend API inside your Swift apps. As a long time backend engineer and iOS developer I've spent a fair amount of time developing backend integrations and iterating on my approach until I found something that worked pretty well. Today, I'd like to share my approach so that it may be useful to new developers or frontend focused developers who want to learn more about integrating with backend services.

To start, I had some goals for this approach that defined my iterations and focus for improvements over time

  1. Avoid libraries unless necessary
  2. The code should be very generic to be easily adaptable to future projects
  3. The code should reflect backend structure in a way that's easy to use and understand
  4. Stick to modern iOS and Swift standards
  5. Focus on JSON REST API's

Service Wrapper

With the goals in mind I decided it would be best to create a generic wrapper class that could handily wrap generic HTTP functions (GET, PUT, POST, etc.) so that this class could simply be copied over into new projects and used to rapidly develop methods for new API's in the future. Further, using URLRequest is pretty simple but it does have some minor adjustments for different HTTP functions that require a fair bit of boilerplate that can be avoided with a service wrapper.

class Backend { 
  // Define the root of our API endpoints 
  let baseUrl: String = "http://localhost:3000" 
  
  func fetch<T: Codable>(url: String, parameters: [String: String]?) async throws -> T { 
    guard var curl = URLComponents(string: "\(baseUrl)\(url)") else { 
      fatalError("Invalid URL") 
    } 
    
    if (parameters != nil) { 
      var query: [URLQueryItem] = [] 
      
      parameters?.forEach({ (key: String, value: String) in 
        query.append(URLQueryItem(name: key, value: value)) 
      }) 
      
      curl.queryItems = query 
    } 
    
    guard let durl = curl.url else { 
      fatalError("Invalid URL") 
    } 
    
    var req = URLRequest(url: durl) 
    let (data, _) = try await URLSession.shared.data(for: req) 
    let decoded = try JSONDecoder().decode(T.self, from: data) 
    
    return decoded 
  }
}

Defining a simple GET function (called fetch because get is a reserved word) we simply set the required parameters, initiate a URLRequest then we attempt to decode the returned value from JSON data into a generic struct T so that we can simply call this function and expect a certain data structure back. We also toss any errors upward instead of handling them here since error handling is a more UI bound task. You'll notice that we're using async here as well, this helps a lot with use in SwiftUI and was added in my most recent iteration so that these methods can be used in .task { } blocks.

💡
If you want to add authentication, you could create an optional parameter, check for it, and if it exists add it to the request like this: req.setValue("Bearer \(token!)", forHTTPHeaderField: "Authorization")

While GET requests are fairly simple because they have no body data there are, of course, more complex HTTP functions we have to cover in our Backend class that will show more of it's usefulness.

func put<T: Codable, B: Codable>(url: String, body: B) async throws -> T { 
  guard let durl = URL(string: "\(baseUrl)\(url)") else { 
    fatalError("Invalid URL") 
  } 
  
  let httpBody = try! JSONEncoder().encode(body) 
  
  var req = URLRequest(url: durl) 
  req.httpMethod = "PUT" 
  req.httpBody = httpBody 
  req.addValue("application/json", forHTTPHeaderField: "Content-Type") 
  req.addValue("application/json", forHTTPHeaderField: "Accept") 
  
  let (data, _) = try await URLSession.shared.data(for: req) 
  let decoded = try JSONDecoder().decode(T.self, from: data) 
  
  return decoded 
}

The put method is a bit more complex, as you can see we added another generic B that is our PUT body. The reason we're using a struct is so that we know exactly what we can and can't send to the API and we can define the data structure when we call this service wrapper later.

func post<T: Codable, B: Codable>(url: String, body: B) async throws -> T { 
  guard let durl = URL(string: "\(baseUrl)\(url)") else { 
    fatalError("Invalid URL") 
  } 
  
  let httpBody = try! JSONEncoder().encode(body) 
  
  var req = URLRequest(url: durl) 
  req.httpMethod = "POST" 
  req.httpBody = httpBody 
  req.addValue("application/json", forHTTPHeaderField: "Content-Type") 
  req.addValue("application/json", forHTTPHeaderField: "Accept") 
  
  let (data, _) = try await URLSession.shared.data(for: req) 
  let decoded = try JSONDecoder().decode(T.self, from: data) 
  
  return decoded 
}

post is similar to put taking the request body type and response type as structs so that we can define requirements later.

func delete(url: String) async throws { 
  guard let durl = URL(string: "\(baseUrl)\(url)") else { 
    fatalError("Invalid URL") 
  } 
  
  var req = URLRequest(url: durl) 
  req.httpMethod = "DELETE" 
  req.addValue("application/json", forHTTPHeaderField: "Content-Type") 
  req.addValue("application/json", forHTTPHeaderField: "Accept") 
  
  _ = try await URLSession.shared.data(for: req)
}

The HTTP DELETE function is similar to GET we don't use a request body here and we typically don't care about a response so long as it is successful. If you wanted a response or to pass parameters you could modify this method to add some features from the get method.

Data Transfer Structs

Now that we have our generic service wrapper written out we can define some data structures for data that's transferred in and out of our app to the backend. I find its easier to work with data in structs because you can model structs similarly to modeling data in a database schema such as Mongoose Schema. Typically a REST API will be structured in a way that endpoints revolve around a specific data type, for example your API may be setup something like this

- Book (/book) 
|- Get Books (GET /book) 
|- Get a Book (GET /book/:id) 
|- Update a Book (POST /book) 
|- Create a Book (PUT /book) 
|- Delete a Book (DELETE /book)

Now, you'll note that in this API definition the object we're working with is Book and all our endpoints are defined on the root <api>/book signifying that this is the data type we're working with. This means that we only have to define one struct that we can use with all of these endpoints and that's a pretty huge time saver. Let's take a look at how a Book struct might look.

struct Book: Codable, Equatable, Hashable { 
  var id: String? 
  var name: String? 
  var author: String? 
  
  static func ==(lhs: Book, rhs: Book) -> Bool { 
    return lhs.id == rhs.id 
  } 
  
  private enum CodingKeys: String, CodingKey { 
    case id = "_id", 
         name, 
         author 
  } 
  
  public init(id: String? = nil, name: String? = nil, author: String? = nil) { 
    self.id = id 
    self.name = name 
    self.author = author 
  }
}

Note that we're making sure our struct is Codable for JSON serialization / deserialization, Equatable so that we can easily check if one Book is equal to another (useful in List) and Hashable which is a little added extra that helps with SwiftUI integration a bit more.

Since we're expecting the backend to send us a generated id for the object we can use that for comparisons to make the equality check more simple. In this example, we're expecting a MongoDB style _id field but because underscore is improper in Swift we just reassign it to id in order to mend with the rest of our Swift codebase.

We're also using var instead of let on our fields with the idea that we will reuse this struct instance to take some data, update it and send it back to the backend. This allows us to reuse this struct for the body of the request and the return type. If you want to be super concise you could define a struct for both request and response so that you can define exactly what can be edited which would be useful if you're writing a public library where users are unfamiliar with your backend code.

API Endpoints

Now that we have created our service wrapper and our structs we can roll our API endpoints using all we've done so far to make them as simple as possible. Since API endpoints are different for every project the less code we have to write for them the better and we've put ourselves in a good spot for just that. Let's create a Book class using our Backend service wrapper and define basic Create, Read, Update and Delete (CRUD) endpoints.

class Books: Backend { 
  // Singleton 
  static let shared = Books() 
  
  func create(_ book: Book) async -> Book? { 
    return try? await put(url: "/book", body: book) 
  } 
  
  func read() async -> [Book]? { 
    return try? await fetch(url: "/book", parameters: nil) 
  } 
  
  func read(id: String) async -> Book? { 
    return try? await fetch(url: "/book/\(id)", parameters: nil) 
  } 
  
  func update(_ book: Book) async -> Book? { 
    return try? await post(url: "/book", body: book)
  } 
  
  func remove(_ id: String) async { 
    try? await delete(url: "/book/\(id)") 
  }
}

As you can see, we've transformed what would be lots of code into a few simple lines for each backend route using our Backend service wrapper and our Book data struct. Now we can extend our framework to support as many data types as we want and copy it over into new projects to rapidly develop the core of REST API integration!

Tags

Steven

Steven has been writing software and exploring computers since the age of 17 all the way back in 2008!