Designing a REST API in Swift
Reiterate uses a backend server to coordinate its in-app purchases. The app and server communicate via a REST API. Here’s how I implemented that in Swift.
Whenever the app wants to take an action that involves the shop in any way, say to get a list of Audio Packs to purchase, or if the user wants to buy some cheer, the app will make a REST call to the server which handles the transaction.
There’s lots of ways to wrap REST transactions into a nice API, and I’ve done that many times over the years. For this instance in particular, it’s the first time I’ve done it with Swift, and I wanted do it using Swift principles.
What I mean by that is I wanted to build a REST interface in a uniquely Swiftian way. Every language has its own idiosyncrasies, its own preferred way of expressing concepts. You can try to fight the language syntax if you rigidly want to write your code your own way, but I think it’s better to try and go with the flow, to write the code the way that the language you’re using wants to be written.
So how does Swift want to be written? What makes Swift code different from, say, Python or C++? There’s a lot of idioms that make Swift “swift,” but in particular I want to focus on a few.
Interfaces, generics, closures, enum with payload. These are some of the syntax facets that, while not unique to Swift, represent some of the most modern tech in compiler and code writing. Swift likes to play on the bleeding edge. It takes a progressive discovery approach. You don’t have to use all of Swift’s advanced features, but you’ll frequently encounter code that takes advantage of it, and when you encounter it for the first time, you can use it cookbook-style or try and take it apart and figure out how all the pieces work. The new SwiftUI interface building code is a great example of this. Some new language features like Result Builders, Opaque Types, and Binding property wrappers were designed specifically to help make SwiftUI code more naturally.
Value Semantics. Swift prefers structs over classes. Internally, collections are all copy-on-write, so everything can be passed around by value instead of by reference.
Lots of sugar, less boilerplate. The Swift designers really seem to want to enable developers to write code that’s a concise and readable as possible. When SwiftUI was being developed, they added Result Builders to the language, so that SwiftUI wouldn’t be bogged down with all the boilerplate that declarative UI frameworks typically come with. There’s all sorts of other syntactic sugar in the language too, like inferred contexts for many method calls, backslash path specs, and optional return keywords. Some of this can go a little too far, and borders on making the language inaccessible to newcomers, but I’ve always appreciated a language that scans cleanly.
What a REST Client Does
The usual situation is you have two pieces of software, a server running on the internet somewhere and a client running on some user machine. In this case, the server is the host you’re connected to right now (I’ve economized a bit, and the blog is served by the same machine that runs the API), and the client is the Reiterate app running on some iPhone. These two programs work in different worlds and it’s the REST Client that acts as a bridge between them.
There’s not a lot going on; I see it as five simple steps
- The application (in this case, Reiterate), makes a call on some Swift object. It’s like any other function call, and the app expects a normal return value like any other function call.
- The REST API translates this call into the approproiate JSON, and sends it over HTML to the server endpoint.
- The server communicates back to the client, via HTTP. The response is almost always another bit of JSON which will need to be unpacked.
- The REST API translates the JSON into the appropriate Swift data structure
- This value gets returned to the main app.
What has gone before
I’ve found a couple already-built frameworks that do this for you, but I had issues with each one.
Here is one tutorial I found that promises to show you “How to build a lightweight REST Library”. The first issue is that it’s pretty old. It doesn’t use the new async/await syntax which was practically made for interacting with network servers. The other problem is that it doesn’t isolate the main app code from all the URL/HTTP/JSON stuff. I want my main code to be completely set apart from all network stuff. This is what calling an endpoint looks like for the first tutorial:
func getUsersList() {
guard let url = URL(string: "https://reqres.in/api/users") else { return }
rest.makeRequest(toURL: url, withHttpMethod: .get) { (results) in
if let data = results.data {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
guard let userData = try? decoder.decode(UserData.self, from: data) else { return }
print(userData.description)
}
}
}
I don’t want any URLs in my code, and I don’t want to have to deal with JSON. Looking back at the diagram, I want the framework to completely isolate the app from anything having to do with JSON or HTTP. The app deals only with Swift objects.
Moya is another REST Library I found, and it has some nice features. You define all the endpoints in an enum, which is great:
enum MyService {
case zen
case showUser(id: Int)
case createUser(firstName: String, lastName: String)
case updateUser(id: Int, firstName: String, lastName: String)
case showAccounts
}
This is a great use of Swift enums and associated values.
But then, defining the rest of the endpoint data seems very boilerplatey:
extension MyService: TargetType {
var baseURL: URL { URL(string: "https://api.myservice.com")! }
var path: String {
switch self {
case .zen:
return "/zen"
case .showUser(let id), .updateUser(let id, _, _):
return "/users/\(id)"
case .createUser(_, _):
return "/users"
case .showAccounts:
return "/accounts"
}
}
var method: Moya.Method {
switch self {
case .zen, .showUser, .showAccounts:
return .get
case .createUser, .updateUser:
return .post
}
}
var task: Task {
switch self {
case .zen, .showUser, .showAccounts: // Send no parameters
return .requestPlain
case let .updateUser(_, firstName, lastName): // Always sends parameters in URL, regardless of which HTTP method is used
return .requestParameters(parameters: ["first_name": firstName, "last_name": lastName], encoding: URLEncoding.queryString)
case let .createUser(firstName, lastName): // Always send parameters as JSON in request body
return .requestParameters(parameters: ["first_name": firstName, "last_name": lastName], encoding: JSONEncoding.default)
}
}
It’s awkward how you have to keep repeating each case for every attribute you want to define. It feels like something is inverted here.
Where Moya falls down is, again, in the handling of the return values:
provider.request(.zen) { result in
switch result {
case let .success(moyaResponse):
let data = moyaResponse.data // Data, your JSON response is probably in here!
let statusCode = moyaResponse.statusCode // Int - 200, 401, 500, etc
// do something in your app
case let .failure(error):
// TODO: handle the error == best. comment. ever.
}
}
I like the initial call. But Moya hasn’t been updated to use the latest async/await syntax, so it’s stuck with closures which is kind of awkward. And again, Moya does nothing with the return values, so it’s up to the client to parse the JSON response.
The other problem with Moya is it’s a wrapper around Alamofire. Alamofire is a pretty big package all on its own, and I’d like to avoid loading up any unnecessary dependencies.
What I would like to see
I want a REST client that handles everything for me. If there’s a balance
endpoint that takes a user
, I want to be able to
call it like this:
let balance = await service.balance(user: user_id)
The rest of this post will show what I came up with. I didn’t get everything I wanted, but I got pretty close.
The Reiterate API
First, let’s start by defining some errors we might throw in response to a request. I’m not trying to cover every possibility here. I’m also not making this a general-purpose library – it’s specific to my needs, and that means there are some hardcoded values in places. I could turn it into a framework if there’s demand for it (let me know in the comments!)
enum ReiterateAPIError: Error {
case badStatusCode
case badResponseType
case badURL
}
Next, we define what a REST endpoint is. For my purposes, a REST endpont is a URL (the path), and a method you use to access that path
(HTTP GET or POST). Additionally, each path contains an api version component. It’s considered good practice to version your endpoint
paths, so you have a sub-path like /v2/
in there, and that lets you quickly change which version of the API you’re accessing,
if you decide to make major changes to how the API works. Then you can keep both versions around on the server side.
struct Restful: Encodable {
enum HTTPMethod: String {
case get = "GET"
case post = "POST"
}
let urlPath: String
let httpMethod: HTTPMethod
var apiVersion: Int
enum CodingKeys: CodingKey {}
init(_ method: HTTPMethod, _ path: String, version: Int = 1) {
urlPath = path
httpMethod = method
apiVersion = version
}
var fullPath: String { "/api/v\(apiVersion)/\(urlPath)" }
var url: URL {
get throws {
guard let fullPathURL = URL(string: "\(ReiterateServer.shared.origin)\(fullPath)") else {
throw ReiterateAPIError.badURL
}
return fullPathURL
}
}
}
Several things to note here.
- We conform to
Encodable
. This lets us use theJSONEncoder
class later on, to automatically take our swift structures and turn them into JSON. All our endpoints will be composed of aRestful
so this needs to conform. However, - We set the
CodingKeys
to an empty dictionary. That means that none of the attributes here in this utility class will actually make it to the final JSON encoding. - We create a convenience
init
method which will be the main way we actually create these structs. - There are some convenience attributes
fullPath
andurl
which package up our attributes into higher-level classes for us to use later.
Now we come to the meat of the API, ReiterateEndpoint
protocol ReiterateEndpoint: Encodable {
var endpoint: Restful { get }
associatedtype ReturnPayloadType: Decodable
}
extension ReiterateEndpoint {
func call() async throws -> ReturnPayloadType {
var request = URLRequest(url: try endpoint.url)
request.httpMethod = endpoint.httpMethod.rawValue
if endpoint.httpMethod == .post {
let encoder = JSONEncoder()
encoder.keyEncodingStrategy = .convertToSnakeCase
request.httpBody = try encoder.encode(self)
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
}
let (data, response) = try await URLSession.shared.data(for: request)
guard let response = response as? HTTPURLResponse, (200...299).contains(response.statusCode) else {
throw ReiterateAPIError.badStatusCode
}
guard let mimeType = response.mimeType, mimeType == "application/json" else {
throw ReiterateAPIError.badResponseType
}
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
return try decoder.decode(ReturnPayloadType.self, from: data)
}
var response: ReturnPayloadType {
get async throws {
return try await call()
}
}
}
We start by defining a protocol
for ReiterateEndpoint
. Anything that is a ReiterateEndpoint
has a Restful
, which is the struct
we just defined which holds the parameters of the REST call, and a ReturnPayloadType
, which will hold the JSON response from the
server.
The call()
function is where everything happens. Using async/await
, we assemble a URLRequest
using the parameters of the given
Restful
endpoint. If this is a .post
endpoint, we assemble the parameters into the httpBody
of the request, encoded into
JSON.
Then the main HTTP call is made, and we check for errors on that. Since everything is done through async/await, we don’t need continuations or any complex code flow. We can simply check for errors and throw them as needed.
Finally, we decode the JSON result from the server, turning it into an appropriate Swift object, and return that. I’ll cover the details on that in a bit.
Using a design like this does have the drawback of having to use .call()
every time you want to make an API call. Since I want to
remove as much boilerplate as possible, I’m adding a convenience property response
which basically does the same thing, but it
allows us to get rid of a pair of parentheses. I feel that’s worth it.
Defining the endpoints
That’s the core of the API. All of the REST Endpoints are defined as substructures of the main ReiterateEndpoint
. Here’s how that works.
Let’s take a look at another REST API, for example, the GitHub API.
One of the endpoints there is List Branches
which gives you all the branches of a given repository. The documentation for that
looks something like this:
GET /repos/{owner}/{repo}/branches
Path Parameters
owner string
The account owner of the repository
repo string
The name of the repository
Response
[
{
"name": "master",
"commit": {
"sha": "c5b97d5ae6c19d5c5df71a34c7fbeeda2479ccbc",
"url": "https://api.github.com/repos/octocat/Hello-World/commits/c5b97d5ae6c19d5c5df71a34c7fbeeda2479ccbc"
},
"protected": true,
"protection": {
"required_status_checks": {
"enforcement_level": "non_admins",
"contexts": [
"ci-test",
"linter"
]
}
},
"protection_url": "https://api.github.com/repos/octocat/hello-world/branches/master/protection"
}
]
For Reiterate, it’s much the same way. For example, when a user purchases cheer from the shop, the PurchaseCheer
endpoint is called.
POST /purchase_cheer
Path Parameters
productID string
The product identifier (from the App Store) for the cheer that was purchased
transactionID string
The approved transaction from Apple’s App Store server
Response
{
"balance": 200
}
Side note: I’ve tried to make the API as robust and secure as possible. That means that the source of truth, ie, where the user’s balance is kept, is on the server side. The balance that’s returned is cached on the user’s device. Also, the server uses the
transactionID
to verify the purchase by independently communicating with Apple’s servers.
Now let’s translate that REST spec into a Swift structure for our API. It looks like this:
struct PurchaseCheer: ReiterateEndpoint {
let endpoint = Restful(.post, "purchase_cheer", version: 2)
let identifier: ProductIdentifier
let transactionId: UInt64
typealias ReturnPayloadType = PurchaseResponse
struct PurchaseResponse: Decodable {
let balance: Int
}
}
I like this because it’s a clean and compact representation of the REST endpoint. We have everything about the endpoint all documented
in one place: the fact that it’s a POST
request, the URL path, the parameters it takes, and what it returns. In fact, I can use this
as the documentation for the endpoint.
But this is more than documentation. This is actual code! This is what defines how values are passed in and out of our REST service. We don’t need anything beyond this in order to call the endpoint. Here’s what that looks like:
if let response = try? await PurchaseCheer(identifier: identifier, transactionId: transaction.id).response {
self.cheerBalance = response.balance
}
That’s it! In my actual app code, I don’t need any URLs, I don’t need to worry about parsing JSON, I only have a simple struct with the values I need, all referenced with clean attributes.
Let’s take a look at a more complicated example. The CheerHistory
endpoint returns a list of every time the user cheered for a product,
and each cheer can (optionally) contain one or more comments. Here’s what that would look like in a REST spec:
GET cheer_history
Path Parameters
userID String
Unique identifier for the user
Response
[{
"creator": "product.creator.name",
"amount": 200,
"product": "product.id",
"date": "2022-08-18-02:30",
"comments": [{
"signature": "user handle",
"body": "comment text"
}],
// ...
}, //...
]
That’s quite a mouthful. In Swift, we can break it up into nice little chunks. First, we define the input parameters and declare the
response as CheerHistoryResponse
without actually defining it first.
struct CheerHistory: ReiterateEndpoint {
let endpoint = Restful(.get, "cheer_history")
let userID: String
typealias ReturnPayloadType = CheerHistoryResponse
}
Now we can say that CheerHistoryResponse
is a list of CheerHistoryEntry
objects, and also factor out the cheer comments into their
own struct.
struct CheerHistoryEntry: Decodable {
let creator: String
let amount: Int
let product: String
let date: Date
let comments: [CheerComment]?
}
struct CheerComment: Decodable {
let signature: String?
let body: String?
}
And then we link it all together by dinally defining what a CheerHistoryResponse
is
struct CheerHistoryResponse: Decodable {
let history: [CheerHistoryEntry]
}
Now when we want to get our cheer history, it’s as simple as this
func getCheerHistory() async -> [CheerHistoryEntry] {
guard let response = try? await CheerHistory(userID: currentUser).response else { return [] }
return response.history
}
I like this solution. It’s not very many lines of code, so there’s no need to pull in some monolithic library dependency. The final code that ends up being called from the main app is clean, just a line or two. We take advantage of async/await to avoid the worst of the exception handling logic, and use Encodable and Decodable goiong in and out of JSON, so we don’t have to deal with that either.
Going Forward
There’s a couple things I’d like to improve.
First, every endpoint ends up being its own top-level struct. I could tuck all those into one master-level struct, but ideally we would want to separate the server-related functionality from the endpoints themselves, so that we have a server-level class with methods for each endpoint.
Having each endpoint as a method seems cleaner stylistically, but practically I couldn’t find a way to have all those methods use the same
boilerplate. As it stands now, each struct inherits the call()
method automatically, so when I define new sub-structs that never has to
be repeated.
I experimented a bit with resultBuilder, but that seems more intended to build “structured data”, and while we are eventually building
a struct
, this still seems like it wouldn’t be appropriate.
What we could do is define a service class with a call()
method that takes the endpoiont structs as arguments. That would end up
looking something like this:
class RESTService {
let baseURL = "https://example.com"
func call<T: RESTEndpoint>(_ method: T) throws -> T.RESTResult {
let encoder = JSONEncoder()
let json = try encoder.encode(method)
let jsonString = String(data: json, encoding: .utf8)
let decoder = JSONDecoder()
// Similar to call() above
let (data, response) = try await URLSession.shared.data(for: request)
// ...
return try decoder.decode(T.RESTResult.self, from: response!)
}
}
protocol RESTEndpoint: Encodable {
associatedtype RESTResult: Decodable
}
struct SpendCheer: RESTEndpoint, Encodable {
let amount: Int
struct SpendResult: Decodable {
let balance: Int
}
typealias RESTResult = SpendResult
}
With this kind of setup, we would create a service instance and then call an endpoint like so:
let service = RESTService()
let balance = try? service.call(SpendCheer(amount: 100))
To my eyes, this is a slightly cleaner abstraction, since we have the service class with endpoints that we actually call. But as far as
syntax goes, it’s a wash. We’re getting rid of the .result
but trading it for some nested parentheses. And the endpoints themselves
are still top-level structs.
I thought there might be some way to use generics to avoid all the boilerplate associated with REST calls, while having the endpoint calls either be an enum or methods on the service class, but I wasn’t able to get it to work. If anyone reading this has a suggestion please let me know in the comments below. For now, I’m satisfied with my API as it is.