Designing a HTTP framework: type safe request
March 08, 2022Previously in Designing a lightweight HTTP framework: foundation we added some functionalities on top of Foundation framework.
This time let's go one step further by enhancing URLRequest
.
Superpower
Like in previous article let's analyse what we have and our needs. URLRequest
has everything to make a request 👍. However what we also want is:
- The ability to have type safe information. Being for the http method, the body, etc...
- Having some information about the return type (later called output)
In order to bring all these functionalities this time we'll create a new type. It's name? Request! 🥸
The design I will use is highly inspired from Building type-safe networking in Swift so I encourage you to read it before.
Here's the final type:
struct Request<Output> {
let path: String
let method: Method
let body: Encodable?
let parameters: [String: String]
private(set) var headers: [HTTPHeader: String] = [:]
}
As you can see it's pretty similar yet still a little bit different from the article I mentioned before. Let's see what changed.
HTTPHeader
HTTPHeader
is a struct encapsulating the header key.
struct HTTPHeader: Hashable, ExpressibleByStringLiteral {
let key: String
}
Why creating such a simple type? 🧐 Big advantage is the ability to define constant headers. Not only will they be available in autocompletion but it avoid me mistyping the header 😅🎉.
extension HTTPHeader {
static let accept: Self = "Accept"
static let authentication: Self = "Authentication"
static let contentType: Self = "Content-Type"
}
Body
I also needed to add a body property for obvious reasons. I decided to make it Encodable
instead of Data
. This avoid passing the decoder manually every time we want to set the property.
Plus it make the property more aligned with our need: we send encodable objects like a user, a review, etc... not an obscure data. So it make apis more expressive.
This came with an interesting challenge though: How to set my body type?
First version was declared as is:
struct Request<Body: Encodable, Output> {
let body: Body?
}
This is because we need a concrete type when using a decoder. Not doing so would lead to an error message "Protocol 'Encodable' as a type cannot conform to the protocol itself"
.
But keeping (and defining) the body type felt useless: we don't care about it. Plus Request<UserBody, UserPayload>
is harder to read than Request<UserPayload>
.
So I decided to use a technique known as "opening an existential": accessing the concrete type from the protocol itself. This open the door of bypassing the generic limitation I faced by adding a method where the protocol encode itself:
extension Encodable {
func encoded<Encoder: TopLevelEncoder>(with encoder: Encoder) throws -> Data {
// here self conform to Encodable so we can use it with generics
try encoder.encode(self)
}
}
Now instead of doing JSONEncoder().encode(someObject)
we can actually do someEncodable.encoded(with: JSONEncoder())
🧙♂️.
This in turn simplified URLRequest.encodedBody
that we defined in the first article:
mutating func encodeBody<Encoder: TopLevelEncoder>(_ body: Encodable, encoder: Encoder) throws {
httpBody = try body.encoded(with: encoder)
// we also use our new `HTTPHeader` type ;)
setHeaders([.contentType: type(of: encoder).contentType.value])
}
And that, kids, is how I made Request.body
being of type Encodable
🥰.
Conclusion
We now have a strongly typed object storing request information. In next article we'll see how to use it alongside URLSession
to perform our requests.