00:06 Today we'll have a first look at the implementation of our Swift Talk backend in Swift! We started rewriting it two years ago, and this version has been online for a while already.
00:28 We want to show how the backend works, but building it from scratch would be a bit boring. So instead, we'll start implementing a new feature, and along the way, we'll explain the different aspects of the backend.
Adding Team Members
00:58 Let's look at the team members page in the account section of the site. When you want to add a person to your team, you have to enter their GitHub username:

This isn't ideal because the team manager might not know the username, which means they'd have to ask the invitee before being able to invite them. We want to reverse this: we want to show a signup link that can be shared with the people who may join your team, which will allow the invitees to sign up with their own GitHub accounts.
01:38 Our first task is to replace the invite form on the team members page with a signup link. When we dive into the code, we find the teamMembersView
function that returns the view to be rendered as a Node
— a recursive enum that represents an HTML node, which can be anything, like an HTML element, a text, or a comment:
func teamMembersView(addForm: Node, teamMembers: [Row<UserData>]) -> Node {
// ...
}
02:23 Inside this function, we find the content definition that's included in the result. We remove the form element and replace it with a paragraph node, Node.p
, with a string as its single child. We also add another paragraph node with a placeholder for the signup link, and we nest the two paragraphs in a div
for styling:
func teamMembersView(addForm: Node, teamMembers: [Row<UserData>]) -> Node {
// ...
let content: [Node] = [
Node.div(classes: "stack++", [
Node.div([
heading("Add Team Member"),
Node.div(classes: "stack", [
Node.p(["To add team members, send them the following signup link:"]),
Node.p(["TODO link"])
])
]),
Node.div([
heading("Current Team Members"),
currentTeamMembers
])
])
]
// ...
}
04:06 When we rebuild the project, we see the changed page:

04:21 We can remove the team members form we used to pass into the teamMembersView
function, as well as the helper that created the form. After doing this, we get a compiler error about the call site in another part of the codebase.
05:00 When the server receives a request from the browser, we turn that request into a Route
— an enum with cases such as the homepage, an episode page, and the team members page. The interpreter then interprets this enum.
We can think of the interpreter as the controller, while the Node
s are comparable to views of an iOS app. Having this separation allows us to swap out the server interpreter with a test interpreter, the latter of which skips all of the server infrastructure.
05:50 In the interpret code, we have a helper function that creates the old team member form, but we no longer need this:
extension Route.Account {
// ...
private func interpret2<I: Interp>(session sess: Session) throws -> I {
func teamMembersResponse(_ data: TeamMemberFormData? = nil, errors: [ValidationError] = []) throws -> I {
let renderedForm = addTeamMemberForm().render(data ?? TeamMemberFormData(githubUsername: ""), errors)
return I.query(sess.user.teamMembers) { members in
I.write(teamMembersView(addForm: renderedForm, teamMembers: members))
}
}
// ...
}
06:23 We remove the helper function, except for its return statement, which we move inline to where we called the helper:
extension Route.Account {
// ...
private func interpret2<I: Interp>(session sess: Session) throws -> I {
switch self {
// ...
case .teamMembers:
let url = Route.teamMemberSignup(token: sess.user.data.teamToken).url
return I.query(sess.user.teamMembers) { members in
I.write(teamMembersView(signupURL: url, teamMembers: members))
}
// ...
}
}
07:04 We were also using the helper function in the route that deletes a team member. Instead of calling the helper to create the response, we redirect back to the team members route:
extension Route.Account {
// ...
private func interpret2<I: Interp>(session sess: Session) throws -> I {
switch self {
// ...
case .deleteTeamMember(let id):
return I.verifiedPost { _ in
I.query(sess.user.deleteTeamMember(id)) {
let task = Task.syncTeamMembersWithRecurly(userId: sess.user.id).schedule(at: globals.currentDate().addingTimeInterval(5*60))
return I.query(task) {
return I.redirect(to: .account(.teamMembers))
}
}
}
}
}
}
07:20 The object we're returning from, I
, is the response type, and one of its helper methods is redirect
. We redirect to another route using the same enum as the one that gets interpreted as a request from the browser. By only representing internal links with an enum, it's impossible to create an incorrect internal link; the compiler simply won't let us.
Generating a Signup Token
08:39 The next step is generating a token for the signup link and saving this token to the database.
We've chosen to use PostgreSQL for our database, and we write our SQL queries manually (with the exception of a few helpers we use to perform some simple queries). We prefer writing a few queries over adding a large abstraction layer that may hide some of the many useful features of SQL.
09:09 An array of queries makes up our database migrations. We add a migration that adds a column for team tokens to the users table:
fileprivate let migrations: [String] = [
// ...
"""
ALTER TABLE users ADD COLUMN IF NOT EXISTS team_token uuid DEFAULT public.uuid_generate_v4();
"""
]
10:04 Since we'll be looking up tokens from the database later on, we also add a token index:
fileprivate let migrations: [String] = [
// ...
"""
CREATE INDEX IF NOT EXISTS team_token_index ON users (team_token);
"""
]
10:33 Each time the server starts, all of the migrations are run. This requires us to pay attention and write our queries in such a way that they can be safely executed many times — note the IF NOT EXISTS
conditions in these two examples.
11:38 We run the server and, not having received any errors, we conclude that the migrations have been performed successfully. So we can now also add the team token to our user model.
Updating the Model
11:59 We use Codable
to automatically generate queries from a struct (which we covered in a previous episode), and also to parse query results back into this struct. Each table is represented by a struct, and we also have some structs for specific queries.
12:35 With all that in place, we now just have to add a teamToken
to the user struct in order to access the token stored in the database:
struct UserData: Codable, Insertable {
var email: String
var githubUID: Int?
// ...
var teamToken: UUID
init(email: String, githubUID: Int? = nil, /*...*/, teamToken: UUID = UUID()) {
self.email = email
self.githubUID = githubUID
// ...
self.teamToken = teamToken
}
static let tableName = "users"
}
13:06 When we run the server and reload the page in the browser, a team token should have been loaded from the database into our user data. But we have no way of knowing, because we're not yet using the token.
13:37 In order to show the signup link, we have to first create a route for it, so we take a look at the Route
enum and its nested enums:
indirect enum Route: Equatable {
case home
case episodes
case sitemap
case subscribe
case collections
case login(continue: Route?)
case account(Account)
// ...
enum Account: Equatable {
case register(couponCode: String?)
case profile
case teamMembers
// ...
}
// ...
}
14:15 The new route we're creating will be similar to the .subscribe
route, with the addition of using a team token in the signup process. We add a new case, called .teamMemberSignup
, with a token as its associated value:
indirect enum Route: Equatable {
// ...
case subscribe,
case teamMemberSignup(token: UUID),
// ...
}
14:39 We simply store the parameters of a Route
in the correct type, like UUID
here, as long as we're able to convert the type from and to a request. By the time we're in one of the interpreting functions, we already have all the parameters we need to process the request.
15:15 We wrote a (slightly complicated) library to support the Route
enum, and we won't go into much detail, but adding a new Route
essentially comes down to specifying how to convert a request to that Route
and how to convert the Route
back to a URL
.
16:10 We do so by providing the router with these two transformations. We first use the constant helper, c
, to tell the router that the URL for this route begins with the string "join_team"
. Then, for the token parameter, we use the /
operator, followed by the Router.uuid
helper, which takes two functions. The first function receives a parsed UUID
and has to return the Route
, and the second one receives a Route
and has to return the UUID
value if it's actually the route we expect:
private let otherRoutes: [Router<Route>] = [
// ...
.c("join_team") / Router.uuid.transform({ .teamMemberSignup(token: $0) }, { route in
guard case let .teamMemberSignup(token) = route else { return nil }
return token
})
]
18:27 Because the library does most of the work of parsing requests (including the parameters) and generating URLs, the main focus has shifted to the conversions between the UUID
parameter and the Route
.
19:00 After adding a new Route
, we have to handle it in the interpreter. The compiler reminds us of this fact because the switch statement in the interpret
function is no longer exhaustive. We add the case and, for now, simply write a string to the response:
extension Route {
func interpret<I: Interp>() throws -> I {
switch self {
// ...
case let .teamMemberSignup(token: token):
return I.write("team signup \(token)")
// ...
}
}
}
19:56 Before we can get to the route, we have to show the signup URL on the team members page, so we add a URL parameter to the teamMembersView
helper:
func teamMembersView(signupURL: URL, teamMembers: [Row<UserData>]) -> Node {
// ...
}
20:17 We remove our placeholder and insert the URL. Before, we used a string literal for a paragraph's child node, which is allowed because the node type implements StringLiteralConvertible
. But now we want to use a string property by wrapping it in a .text
node. We also specify a CSS class to give the link a monospaced font:
func teamMembersView(signupURL: URL, teamMembers: [Row<UserData>]) -> Node {
// ...
let content: [Node] = [
Node.div(classes: "stack++", [
Node.div([
heading("Add Team Member"),
Node.div(classes: "stack", [
Node.p(["To add team members, send them the following signup link:"]),
Node.p(classes: "type-mono", [.text(signupURL.absoluteString)])
])
]),
// ...
])
]
// ...
}
21:19 When we try to run the server, the view helper complains about the fact that we're not yet passing in a signup URL, so we take the URL from the route we just added:
extension Route.Account {
// ...
private func interpret2<I: Interp>(session sess: Session) throws -> I {
switch self {
// ...
case .teamMembers:
let url = Route.teamMemberSignup(token: sess.user.data.teamToken).url
return I.query(sess.user.teamMembers) { members in
I.write(teamMembersView(signupURL: url, teamMembers: members))
}
// ...
}
}
}
22:05 When we run the server again and refresh, we see the signup link on the team members page:

22:11 We copy the URL and open it in the browser to see the response we wrote earlier:

22:24 We can try messing with the URL and remove a character from the token; this results in a "page not found" error. That's because the router tries to parse the string "join_team"
followed by a UUID, and if it can't, there's no route that matches with the URL.
It's a good first check that the route only works with a valid UUID. However, we're not yet checking whether the requested UUID is in fact a valid token in the database.
Discussion
23:22 So far, we've already seen a few different parts of our backend's infrastructure: we modified a view, we added a database migration and updated our database model, and we added a new route and a minimal response for it.
23:39 Everything is built directly on top of SwiftNIO. Not using any other frameworks in between makes some parts, like driving the database, pretty bare bones. But this also helps us remain efficient: we can write exactly the queries we need. And SQL itself is a high-level language, which we couldn't write better ourselves.
24:42 In the upcoming episodes, we'll finish the team token signup flow, for which we'll have to query the database. We'll also add a button to invalidate the signup link by generating a new token, and we'll write a few tests at some point.