My Thoughts on Server-Side Swift: Routing

The folks over at objc.io put out another high quality video, this time on Server-Side Swift: Routing. I like the high level goal they are going for, but I actually approach the problem differently.

To me, the key of setting up good architecture is leveraging what you are given while keeping the boundaries between the libraries or frameworks that you are using and your own code.

For example, I’m working on a server-side Swift project for D&D. The full conceptual flow looks like for the scenario of a user looking up a spell with a given name:

  1. User makes a GET request to /spells/burning-hands
  2. Vapor looks up the route and invokes the handler
  3. Handler does conversions and passes to my own SpellRouter.get() function
  4. This function handles the non-Vaporized data calling into my lookup code: Spell.lookup(name:)
  5. Above function returns an array of [Spell] items
  6. The [Spell] is converted into an SpellCard which serves as an abstract HTML representation
  7. That gets rendered into raw HTML
  8. The raw HTML makes it back to the Vapor route handler
  9. A Vapor Response is returned
  10. The user sees the HTML page

It looks like a lot of steps, but it has the following benefits:

  1. Each layer is agnostic of the layer below it. That is, my logic layer knows nothing about Vapor, HTML, or HTTP; it only knows how to perform spell lookups based on strongly-typed parameters, this case: name.
  2. The boiler plate is essentially nonexistent.
  3. It maintains flexibility without forcing a rigid structure. Swapping out Vapor for another web framework is a matter of writing the conversion functions.
  4. It scales well to multiple rendering needs.

Let’s go over some code to help make things more clear.

Server Hookup

drop.get("/spells", String.self) { req, name in
    SpellRouter.get(name: name).html
}

drop.get("/api/spells/", String.self) { req, name in
    SpellRouter.api(name: name).json
}

The variable drop is a Vapor Droplet instance. This code is in my main.swift file. A couple of things to note:

  1. I do not abstract the path (e.g. /spells) out somewhere else. The server is what needs to know about paths, so this is the level it should exist at.
  2. Vapor pulls out the first parameter as a String because of my usage of the second parameter in the get() function.
  3. In my case, there is very little conversion that I need to go from Vapor specifics to my own routing code, just handling the name retrieval, which is done above.

The only other Vapor specific items are the html and json extensions that I have on my own HtmlResponse and JsonResponse types that get() and api() return from the SpellRouter type.

They look like this:

extension HtmlResponse {
    var html: Response {
        return Response(status: .ok,
                       headers: ["Content-Type": "text/html"],
                          body: self.element.html)
    }
}

extension JsonResponse {
    var json: Response {
        return Response(status: .ok,
                       headers: ["Content-Type": "text/json"], 
                          body: self.json)
    }
}

The Response class is a Vapor type. But that’s it, the main.swift file here is the only place Vapor is referenced.

Spell Routing

For my routing, I implement the same get() and post() style APIs. This makes the translation process fairly straight forward.

The routing function looks a bit like this:

static func get(name: String) -> HtmlResponse {
    let translatedName = decode(name: name).lowercased()
    let spells = Spell.lookup(name: translatedName)
    if spells.count != 1 {
        return InvalidHtmlResponse(message: "No spells were found with the name: '\(name)'")
    }

    let spell = spells[0]
    return SpellCard(spell: spell)
}

Basically, and incoming request might look like this: /spells/burning-hands. The decode() function converts the name from “burning-hands” to “burning hands”. Then I call into my spell DB via the lookup(name:) function. And in this case, I only return the first spell found via a SpellCard class that is an abstracted HTML view of the card. The api() function returns a SpellApi instead.

In the End

Anyhow, I prefer this approach over the enum-style approach that was described. For one, it makes handling different types of requests (GET and POST for example) at the same URL extremely easy. They’ll have to encode that data somehow into their enum to handle it.

I’m interested to see how they progress through the problem though to handle more of the necessary aspects.

My Thoughts on Server-Side Swift: Routing