Architecting a Code to Create Different Types of URLRequest
Evangelist Apps
Experts in mobile app Development & Design for all iOS (iPhones, iPads, Apple Watches & Apple TV) and Android platforms
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:
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!