Architecting a Code to Create Different Types of URLRequest

Evangelist Apps
Evangelist Apps Blog
7 min readMay 31, 2023

By Raviraj Wadhwa, Senior iOS developer at Evangelist Apps.

Introduction:

In every app, we often need to make various API calls, each with its unique requirements. These calls involve creating different URLRequests. The URL paths, HTTP methods, and data passed in these requests can vary. To simplify the process of creating requests, let’s structure our code in a way that allows for easy request creation.

Example: Handling Employee-related Data

Let’s consider an app that deals with employee data. This app needs to make several API calls to perform actions such as retrieving all employees, fetching a specific employee, creating a new employee, updating employee details, deleting an employee, and searching for employees. To handle these different types of calls, we’ll start by creating an enum representing the various API requests.

typealias EmployeeId = Int
typealias EmployeeName = String
typealias EmployeeSalary = Double
typealias EmployeeAge = Int

// MARK: - Employee

struct Employee: Codable {
// Employee properties
let name: String?
let salary: Double?
let age: Int?
let id: Int?

enum CodingKeys: String, CodingKey {
case name = "name"
case salary = "salary"
case age = "age"
case id = "id"
}
}

// MARK: - Enum

enum EmployeeRequests {
case getAllEmployees
case getEmployee(EmployeeId)
case createEmployee(Employee)
case updateEmployee(EmployeeId, Employee)
case deleteEmployee(EmployeeId)
case searchEmployees(EmployeeName?, EmployeeSalary?, EmployeeAge?)
}

In this code snippet, note the following points:

  1. Each enum case represents a different API request, with varying inputs.
  2. Typealiases are used to make the input requirements self-explanatory.
  3. The basic idea is to categorize different sets of requests using enums. Similarly, for requests related to orders, customers, or users, we would create separate enums.

Creating URLRequestCreator Protocol:

Now whenever we need to make a call we need to create a request and we will create a request like this.

let request = EmployeeRequests.getAllEmployees.create

However, currently, the compiler throws an error because the member function is not yet implemented.

To resolve this, we need to define a member function called create within our enum. We'll begin by creating a protocol called URLRequestCreator.

protocol URLRequestCreator {

associatedtype BodyObject: Encodable

var baseUrlString: String { get }
var apiPath: String? { get }
var apiVersion: String? { get }
var endPoint: String? { get }
var queryString: String? { get }
var queryItems: [URLQueryItem]? { get }
var method: HTTPRequestMethod { get }
var headers: [String: String]? { get }
var bodyParameters: [String: Any]? { get }
var bodyObject: BodyObject? { get }

}

extension URLRequestCreator {

func create() throws -> URLRequest {

guard var urlComponents = URLComponents(string: baseUrlString) else {
throw URLRequestCreatorError.failedToCreateURLComponents
}

var fullPath = ""

if let apiPath {
fullPath += "/" + apiPath
}

if let apiVersion {
fullPath += "/" + apiVersion
}

if let endPoint {
fullPath += "/" + endPoint
}

if let queryString {
fullPath += "/" + queryString
}

urlComponents.path = fullPath

urlComponents.queryItems = queryItems

guard let url = urlComponents.url else {
throw URLRequestCreatorError.failedToGetURLFromURLComponents
}
var request = URLRequest(url: url)
request.httpMethod = method.rawValue

if let headers {
for header in headers {
request.addValue(header.value, forHTTPHeaderField: header.key)
}
}

let jsonData = try? JSONEncoder().encode(bodyObject)
request.httpBody = jsonData

return request

}

}

enum URLRequestCreatorError: Error {
case failedToCreateURLComponents
case failedToGetURLFromURLComponents
}

This protocol defines the required information for creating a URLRequest, such as the base URL, path, HTTP method, headers, and body. The create method concatenates the provided information to construct the URLRequest.

Our protocol is designed in a way — that whoever adopts it will need to provide this info and then the common create method will concatenate all the info provided to create a URLRequest.

Note that:

1. Apart from baseUrlString everything else is optional. Because not all APIs will have the same level of path or query attached to it or body attached to it or headers.

2. In create method we are making sure if the particular info is available or not. Appending it into the request only if it is available.

3. The create method can throw an error. If we are passing valid info then it won't throw an error but in case by mistake we passed incorrect values it will throw an error and help us in pointing out the issue.

4. We have a generic associatedtype BodyObject: Encodable. The reason we are using genric type here is that some EmployeeRequests need to pass value of type Employee. But this protocol can be adopted by various requuest and they might need to pass value of some other type. e.g. OrderRequests might need to pass value of type Order.

5. You might also be wondering why we don’t have a create function inside the protocol itself. Why we have written it in a separate extension? Well, that is because protocol just holds the methods or properties declaration, not their actual body. If we try to write create method inside protocol then we will get a compiler error: Protocol methods must not have bodies. However, through extension, we can extend a protocol and add common methods to it which can be called by any instance adopting the protocol.

Implementing the URLRequestCreator Protocol:

We’ll now implement the URLRequestCreator protocol in our EmployeeRequests enum. By adopting this protocol, we'll be able to create requests easily.

As you can see as soon as the EmployeeRequests adopts the protocol URLRequestCreator the compiler throws an error and tells us to add the protocol stubs. That is define the properties declared in the protocol. That is provide the values required by the protocol.

So the next point is now to provide the info/values required to create requests.

extension EmployeeRequests: URLRequestCreator {
typealias BodyObject = Employee

var baseUrlString: String {
"https://dummy.restapiexample.com"
}

// Define other properties required for creating URLRequest

var apiPath: String? {
"api"
}

var apiVersion: String? {
"v1"
}

var endPoint: String? {
switch self {
case .getAllEmployees:
return "employees"
case .getEmployee:
return "employee"
case .createEmployee:
return "create"
case .updateEmployee:
return "update"
case .deleteEmployee:
return "delete"
case .searchEmployees:
return "search"
}
}

var queryString: String? {
switch self {
case .getEmployee(let employeeId), .updateEmployee(let employeeId, _), .deleteEmployee(let employeeId):
return String(employeeId)
default:
return nil
}
}

var queryItems: [URLQueryItem]? {
switch self {
case .searchEmployees(let employeeName, let employeeSalary, let employeeAge):
var queryItems = [URLQueryItem]()
if let employeeName {
queryItems.append(URLQueryItem(name: "name", value: employeeName))
}
if let employeeSalary {
queryItems.append(URLQueryItem(name: "salary", value: String(employeeSalary)))
}
if let employeeAge {
queryItems.append(URLQueryItem(name: "age", value: String(employeeAge)))
}
return queryItems
default:
return nil
}
}

var method: HTTPRequestMethod {
switch self {
case .getAllEmployees:
return .GET
case .getEmployee:
return .GET
case .createEmployee:
return .POST
case .updateEmployee:
return .PUT
case .deleteEmployee:
return .DELETE
case .searchEmployees:
return .GET
}
}

var headers: [String: String]? {
switch self {
case .createEmployee, .updateEmployee:
return ["Content-Type": "application/json"]
default:
return nil
}
}

var bodyParameters: [String : Any]? {
return nil
}

var bodyObject: Employee? {
switch self {
case .createEmployee(let employee), .updateEmployee(_, let employee):
return employee
default:
return nil
}
}
}

In the above code, we provide the necessary information for request creation within the EmployeeRequests enum. Each case in the enum corresponds to a specific request, and we specify the appropriate values for the URLRequest properties.

Creating and Using Requests:

With the create method implemented, we can now easily create and use requests. Here are some examples:

do {
let request1 = try EmployeeRequests.getAllEmployees.create()
// Request created
// Make a call
} catch {
// Error in request creation
}

if let request2 = try? EmployeeRequests.getEmployee(1).create() {
// Request created
// Make a call
} else {
// Error in request creation
}

let request3 = try? EmployeeRequests.deleteEmployee(1).create()

let request4 = try? EmployeeRequests.searchEmployees("test", 1000, nil).create()

let employee = Employee(
name: "test",
salary: 123456,
age: 18,
id: nil
)

let request5 = try? EmployeeRequests.createEmployee(employee).create()

let request6 = try? EmployeeRequests.updateEmployee(1, employee).create()

Conclusion:

By following this approach, we can architect our code to easily create different types of URLRequest based on the specific API requirements. The protocol-oriented design allows us to define common properties and methods, reducing duplication and providing a clean and scalable structure.

Tips:

When the base URL is the same for multiple APIs, define it as a default value in the protocol extension.

/// Here we are passing default base URL that most of the apis in app will have
/// However in some case different module may have different base URL
/// Then that different base URL can be provided from that module.
extension URLRequestCreator {
var baseUrlString: String {
"https://dummy.restapiexample.com"
}
}

By doing so we do not need to pass the base URL from EmployeeRequests. It becomes optional and that is when we delete the code of providing the base URL and still the build succeeds.

Similarly, consider creating enums for common values, such as HTTP methods or API versions, to improve code clarity and maintainability. Example:

enum HTTPRequestMethod: String {
case GET
case POST
case PUT
case DELETE
case PATCH
}

enum HTTPRequestAPIVersion: String {
case v1
case v2
case v3
}

Usage:

protocol URLRequestCreator {
var apiVersion: HTTPRequestAPIVersion? { get }
var method: HTTPRequestMethod { get }
}

extension EmployeeRequests: URLRequestCreator {
var apiVersion: HTTPRequestAPIVersion? {
.v1
}

var method: HTTPRequestMethod {
switch self {
case .getAllEmployees:
return .GET
case .getEmployee:
return .GET
case .createEmployee:
return .POST
case .updateEmployee:
return .PUT
case .deleteEmployee:
return .DELETE
case .searchEmployees:
return .GET
}
}
}

We hope this guide helps you structure your code for creating URLRequest efficiently. Your feedback is highly appreciated!

And here is the complete project URL: https://github.com/ravirajw/NetworkingClient

Thanks for reading!

Please follow us on Twitter and LinkedIn for more updates.

#SwiftUI #Swift.org #evangelistapps #TheComposableArchitecture #swift #iOS #coding #iOS16

--

--

Evangelist Apps
Evangelist Apps Blog

Evangelist Software is UK based mobile apps development agency that specializes in developing and testing iOS apps.