Designing a HTTP framework: type safe request

Tue, 8 Mar 2022 00:00:00 +0000

Let's talk about how to build a type safe request for our network framework

Previously 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.