Blog

How to use ApiKit to integrate with a REST API

Jan 17, 2024 swiftapi

In this post, we’ll take a look at how easy it is to use the open-source Swift package ApiKit to integrate with a REST-based API and map its data to local models.

Header image

ApiKit builds on a basic concept of environments and routes and provides lightweight types that make it easy to integrate with any REST-based APIs.

ApiKit has ApiEnvironment and ApiRoute protocols that make it easy to model an API, and an ApiRequest that can define a route and a response type for even easier use.

ApiKit can use a regular URLSession or a custom ApiClient to fetch any route and request from any environment. You can define custom data for both routes and environments.

API environments

An ApiEnvironment refers to a specific API version or environment (prod, staging, etc.) and defines a URL as well as global request headers and query parameters.

For instance, this is all it takes to model the Yelp v3 API, which requires an API token:

import ApiKit

enum YelpEnvironment: ApiEnvironment {

    case v3(apiToken: String)
    
    var url: String {
        switch self {
        case .v3: "https://api.yelp.com/v3/"
        }
    }
 
    var headers: [String: String]? {
        switch self {
        case .v3(let token): ["Authorization": "Bearer \(token)"]
        }
    }
    
    var queryParams: [String: String]? {
        [:]
    }
}

The Yelp API requires that all requests send an API token as a custom header. Other APIs may require a token to be sent as a query parameter, or have no such requirements at all.

ApiKit is flexible and supports many different requirements.

API routes

An ApiRoute refers to an API endpoint. It defines an HTTP method (GET, POST, etc.), an environment-relative path, query parameters, post data, etc.

For instance, this Yelp route enum defines how to fetch and search for restaurants:

import ApiKit

enum YelpRoute: ApiRoute {

    case restaurant(id: String)
    case search(params: Yelp.SearchParams)

    var path: String {
        switch self {
        case .restaurant(let id): "businesses/\(id)"
        case .search: "businesses/search"
        }
    }

    var httpMethod: HttpMethod { .get }

    var headers: [String: String]? { nil }

    var formParams: [String: String]? { nil }

    var postData: Data? { nil }
    
    var queryParams: [String: String]? {
        switch self {
        case .restaurant: nil
        case .search(let params): params.queryParams
        }
    }
}

The routes above use associated values to provide IDs to the restaurant path and search parameters as query parameters.

Since both environments and routes can specify headers, query parameters, etc., a route will complete or replace the environment’s parameters.

API models

We also have to define Codable Yelp-specific models to be able to map data from the API.

For instance, this is a super lightweight Yelp restaurant model representation:

struct YelpRestaurant: Codable {
    
    public let id: String
    public let name: String?
    public let imageUrl: String?
    
    enum CodingKeys: String, CodingKey {
        case id
        case name
        case imageUrl = "image_url"
    }
}

The id and name parameters use the same name as in the API, while imageUrl requires a custom mapping definition.

How to fetch data from an API

We can now fetch data from the Yelp API, using a URLSession or any custom ApiClient:

let client = URLSession.shared
let env = YelpEnvironment.v3(apiToken: "TOKEN") 
let route = YelpRoute.restaurant(id: "abc123") 
let restaurant: YelpRestaurant = try await client.fetchItem(at: route, in: env)

This code will fetch data from the API and either return a mapped result or throw an error.

How to fetch data even easier

We can also define a ApiRequest to couple a route to a certain response model:

struct YelpRestaurantRequest: ApiRequest {

    typealias ResponseType = YelpRestaurant

    let id: String

    var route: ApiRoute { 
        YelpRoute.restaurant(id: id)
    }
}

We can now fetch data with our request, without having to define a return type and route:

let client = URLSession.shared
let env = YelpEnvironment.v3(apiToken: "TOKEN") 
let request = YelpRestaurantRequest(id: "abc123") 
let restaurant = try await client.fetch(request, in: env)

Requests require extra code, but reduce the risk of model and route mismatching.

Conclusion

ApiKit makes it easy to integrate with any REST-based API. The environments and routes are easily defined, and the code easy to read and debug.

You can have a look at the built-in integrations and the demo app. I really hope you like it.

Discussion

If you found this post interesting and would like to share your thoughts, ideas, feedback etc. please comment in the Disqus section below or reply to this tweet or this toot.