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:
- User makes a
GET
request to/spells/burning-hands
- Vapor looks up the route and invokes the handler
- Handler does conversions and passes to my own
SpellRouter.get()
function - This function handles the non-Vaporized data calling into my lookup code:
Spell.lookup(name:)
- Above function returns an array of
[Spell]
items - The
[Spell]
is converted into anSpellCard
which serves as an abstract HTML representation - That gets rendered into raw HTML
- The raw HTML makes it back to the Vapor route handler
- A Vapor
Response
is returned - The user sees the HTML page
It looks like a lot of steps, but it has the following benefits:
- 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.
- The boiler plate is essentially nonexistent.
- It maintains flexibility without forcing a rigid structure. Swapping out Vapor for another web framework is a matter of writing the conversion functions.
- 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:
- 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. - Vapor pulls out the first parameter as a
String
because of my usage of the second parameter in theget()
function. - 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.