Vapor 4 — Server Side Swift

From Vapor 3 to 4: Elevate your server-side app

Radu Dan
11 min readNov 10, 2020

--

In this article, we will explore how to migrate an application developed in Vapor 3 to the latest version, Vapor 4.

Short recap

We saw together in how we can develop a basic REST API in Vapor 3.

We covered in this article (ℹ️ or you can read it on my personal website — link here) how to develop a basic REST API using Vapor 3.

The server-side app structure in Vapor 3 is as follows:

├── Public
├── Sources
│ ├── App
│ │ ├── Controllers
│ │ ├── Models
│ │ ├── boot.swift
│ │ ├── configure.swift
│ │ └── routes.swift
│ └── Run
│ └── main.swift
├── Tests
│ └── AppTests
└── Package.swift

Package.swift

  • This file is the project’s manifest and defines all dependencies and targets for the app.
    The project is set to use Vapor 3.3.0:
    .package(url: "https://github.com/vapor/vapor.git", from: "3.3.0")

Public

  • Contains all public resources, such as images.

Sources

  • Contains two separate modules: App and Run.
    The App folder is where you put all your developed code.
    The Run folder contains the main.swift file.

Models

  • This is where you add your Fluent models. In this app, the models are: User, Player, Gather.

Controllers

  • Controllers contain the logic of your REST API, such as CRUD operations.
    They are similar to iOS ViewControllers but handle requests and manage models.

routes.swift

  • Used to find the appropriate response for an incoming request.

configure.swift

  • This file is called before the app is initialized. It registers the router, middlewares, database, and model migrations.

When the server app runs, the sequence is:
main.swiftapp.swift (to create an instance of the app) → configure.swift (called before the app initializes) → routes.swift (handles route registration) → boot.swift (called after the app is initialized).

Migration

Package

A lot has changed from Vapor 3, and migrating to Vapor 4 is not as straightforward as it might seem.

Below, we present the differences in the Package.swift file. You can also check it out on GitHub.

Code diffs of Package.swift between Vapor 3 and 4

Updating our Models

Vapor 4 harnesses the full potential of Swift and includes property wrappers at its core.
The Model protocol now replaces the previous SQLiteTypeModel that was extended in Vapor 3.
Additionally, models no longer need to implement Migration.

We also remove the SQLiteUUIDPivot protocol and update PlayerGatherPivot to implement the standard Model protocol.

The foreign and private keys are now defined using the @Parent property wrapper.

You can see below the transformations applied to the codebase:

  • Gather model transformation — commit
  • Player model transformation — commit
  • Pivot model transformation — commit
  • Token model transformation — commit
  • User model transformation — commit

Here is an example of the updated PlayerGatherPivot model:

import Vapor
import FluentSQLiteDriver

// MARK: - Model
final class PlayerGatherPivot: Model {
static let schema = "player_gather"
@ID(key: .id)
var id: UUID?
@Parent(key: "player_id")
var player: Player
@Parent(key: "gather_id")
var gather: Gather
@Field(key: "team")
var team: String
init() {}
init(playerID: Player.IDValue,
gatherID: Gather.IDValue,
team: String) {
self.$player.id = playerID
self.$gather.id = gatherID
self.team = team
}
}
// MARK: - Migration
extension PlayerGatherPivot: Migration {
func prepare(on database: Database) -> EventLoopFuture {
database.schema(PlayerGatherPivot.schema)
.field("id", .uuid, .identifier(auto: true))
.field("player_id", .int, .required)
.field("gather_id", .uuid, .required)
.field("team", .string, .required)
.create()
}
func revert(on database: Database) -> EventLoopFuture {
database.schema(PlayerGatherPivot.schema).delete()
}
}

Updating our Controllers

Route collections still use the boot function, but the function’s parameter type has changed from Router to RoutesBuilder.

We no longer use Model.parameter. Instead, we now use :id as a parameter marker.

Fetching the authenticated user has changed from this in Vapor 3:

let user = try req.requireAuthenticated(User.self)
return try user.gathers.query(on: req).all()

To this in Vapor 4:

let user = try req.auth.require(User.self)

Regarding authentication, the middleware setup has also changed:

let tokenAuthMiddleware = Token.authenticator()
let guardMiddleware = User.guardMiddleware() // same as Vapor 3
let tokenAuthGroup = gatherRoute.grouped(tokenAuthMiddleware, guardMiddleware)

The token authentication middleware now implements a new protocol called ModelTokenAuthenticatable:

extension Token: ModelTokenAuthenticatable {
static let valueKey = \Token.$token
static let userKey = \Token.$user

var isValid: Bool { true }
}

You can check all Vapor 4 controller transformations on GitHub:

The CRUD operations have undergone significant changes:

GET All Gathers

func getGathersHandler(_ req: Request) throws -> EventLoopFuture<[GatherResponseData]> {
let user = try req.auth.require(User.self)
return user.$gathers.query(on: req.db)
.all()
.flatMapEachThrowing {
try GatherResponseData(
id: $0.requireID(),
userId: user.requireID(),
score: $0.score,
winnerTeam: $0.winnerTeam
)
}
}

Note that flatMapEachThrowing is used to apply a closure to each element in the sequence, which is wrapped in an EventLoopFuture.

CREATE a Gather

func createHandler(_ req: Request) throws -> EventLoopFuture {
let user = try req.auth.require(User.self)
let gather = try Gather(userID: user.requireID())
return gather.save(on: req.db).map {
let response = Response()
response.status = .created
if let gatherID = gather.id?.description {
let location = req.url.path + "/" + gatherID
response.headers.replaceOrAdd(name: "Location", value: location)
}
return response
}
}

DELETE a Gather

func deleteHandler(_ req: Request) throws -> EventLoopFuture {
let user = try req.auth.require(User.self)

guard let id = req.parameters.get("id", as: UUID.self) else {
throw Abort(.badRequest)
}
return Gather.find(id, on: req.db).flatMap {
guard let gather = $0 else {
throw Abort(.notFound)
}
return gather.delete(on: req.db).transform(to: .noContent)
}
}

UPDATE a gather

func updateHandler(_ req: Request) throws -> EventLoopFuture {
let user = try req.auth.require(User.self)
let gatherUpdateDate = try req.content.decode(GatherUpdateData.self)

guard let id = req.parameters.get("id", as: UUID.self) else {
throw Abort(.badRequest)
}

return user.$gathers.get(on: req.db).flatMap { gathers in
guard let gather = gathers.first(where: { $0.id == id }) else {
return req.eventLoop.makeFailedFuture(Abort(.notFound))
}

gather.score = gatherUpdateDate.score
gather.winnerTeam = gatherUpdateDate.winnerTeam

return gather.save(on: req.db).transform(to: .noContent)
}
}

GET players of a specified gather

func getPlayersHandler(_ req: Request) throws -> EventLoopFuture<[PlayerResponseData]> {
let user = try req.auth.require(User.self)
guard let id = req.parameters.get("id", as: UUID.self) else {
throw Abort(.badRequest)
}

return user.$gathers.get(on: req.db).flatMap { gathers in
guard let gather = gathers.first(where: { $0.id == id }) else {
return req.eventLoop.makeFailedFuture(Abort(.notFound))
}

return gather.$players.query(on: req.db)
.all()
.flatMapEachThrowing {
try PlayerResponseData(
id: $0.requireID(),
name: $0.name,
age: $0.age,
skill: $0.skill,
preferredPosition: $0.preferredPosition,
favouriteTeam: $0.favouriteTeam
)
}
}
}

POST player to a specified gather

func addPlayerHandler(_ req: Request) throws -> EventLoopFuture {
let user = try req.auth.require(User.self)
let playerGatherData = try req.content.decode(PlayerGatherData.self)

guard let gatherID = req.parameters.get("gatherID", as: UUID.self) else {
throw Abort(.badRequest)
}

guard let playerID = req.parameters.get("playerID", as: Int.self) else {
throw Abort(.badRequest)
}

let gather = user.$gathers.query(on: req.db)
.filter(\.$id == gatherID)
.first()

let player = user.$players.query(on: req.db)
.filter(\.$id == playerID)
.first()

return gather.and(player).flatMap { _ in
let pivot = PlayerGatherPivot(
playerID: playerID,
gatherID: gatherID,
team: playerGatherData.team
)

return pivot.save(on: req.db).transform(to: .ok)
}
}

Models in Vapor 4

The application has the following registered models: User, Gather, Player, PlayerGatherPivot, and Token.

User Model

The User model has three main properties:

  • id: A UUID that serves as the primary key.
  • username: A unique name created during registration.
  • password: The hashed password of the user.

An inner class, User.Public, is defined to control what public information (just the username) is exposed through methods such as GET.

The one-to-many relationships with Gather (a user can create multiple gatherings) and with Player (a user can create multiple players) are implemented using the Fluent @Children property wrapper:

final class User: Model {
static let schema = "users"

@ID(key: .id)
var id: UUID?

@Field(key: "username")
var username: String

@Field(key: "password")
var password: String

@Children(for: \.$user)
var gathers: [Gather]

@Children(for: \.$user)
var players: [Player]

init() {}

init(id: UUID? = nil,
username: String,
password: String) {
self.id = id
self.username = username
self.password = password
}
}

We use extensions to wrap our functions for transforming a normal User to User.Public and vice versa:

// MARK: - Public User
extension User {
final class Public {
var id: UUID?
var username: String

init(id: UUID?, username: String) {
self.id = id
self.username = username
}
}
}

extension User.Public: Content {}

extension User {
func toPublicUser() -> User.Public {
return User.Public(id: id, username: username)
}
}

Token Model

The Token model defines a mapping between a generated 16-byte data (the actual token) that is base64 encoded and the user ID.

To generate the token, we use the CryptoRandom().generateData function, which relies on OpenSSL RAND_bytes to generate random data of the specified length.

The authentication pattern for the server application is Bearer token authentication.
Vapor simplifies this process by requiring the implementation of the BearerAuthenticatable protocol and specifying the key path of the token key:
static let tokenKey: TokenKey = \Token.token.

final class Token: Model {
static let schema = "tokens"

@ID(key: .id)
var id: UUID?

@Field(key: "token")
var token: String

@Parent(key: "user_id")
var user: User

init() {}

init(id: UUID? = nil,
token: String,
userID: User.IDValue) {
self.id = id
self.token = token
self.$user.id = userID
}
}

// MARK: - Authenticable
extension Token: ModelTokenAuthenticatable {
static let valueKey = \Token.$token
static let userKey = \Token.$user

var isValid: Bool { true }
}

Gather Model

The Gather model represents our football matches.

It has a parent identifier (the user ID) and two optional string parameters: the score and the winning team.

final class Gather: Model {
static let schema = "gathers"

@ID(key: "id")
var id: UUID?

@Parent(key: "user_id")
var user: User

@OptionalField(key: "score")
var score: String?

@OptionalField(key: "winner_team")
var winnerTeam: String?

@Siblings(through: PlayerGatherPivot.self, from: \.$gather, to: \.$player)
var players: [Player]

init() {}

init(id: UUID? = nil,
userID: User.IDValue,
score: String? = nil,
winnerTeam: String? = nil) {
self.id = id
self.$user.id = userID
self.score = score
self.winnerTeam = winnerTeam
}
}

Player Model

The Player model is defined similarly to the Gather model:

  • userID: The ID of the user who created this player (the parent).
  • name: Combines the first and last names.
  • age: An optional integer to store the player's age.
  • skill: An enum specifying the player's skill level (beginner, amateur, or professional).
  • preferredPosition: Represents the position on the field that the player prefers.
  • favouriteTeam: An optional string parameter to record the player's favorite team.
final class Player: Model {
static let schema = "players"

@ID(custom: \.$id)
var id: Int?

@Parent(key: "user_id")
var user: User

@Field(key: "name")
var name: String

@OptionalField(key: "age")
var age: Int?

@OptionalField(key: "skill")
var skill: Skill?

@OptionalField(key: "position")
var preferredPosition: Position?

@OptionalField(key: "favourite_team")
var favouriteTeam: String?

@Siblings(through: PlayerGatherPivot.self, from: \.$player, to: \.$gather)
public var gathers: [Gather]

convenience init() {
self.init(userID: UUID(), name: "")
}

init(id: Int? = nil,
userID: User.IDValue,
name: String,
age: Int? = nil,
skill: Skill? = nil,
preferredPosition: Position? = nil,
favouriteTeam: String? = nil) {
self.id = id
self.$user.id = userID
self.name = name
self.age = age
self.skill = skill
self.preferredPosition = preferredPosition
self.favouriteTeam = favouriteTeam
}
}

Relationships

Vapor 3

The many-to-many relationship between gathers and players (where one gather can have multiple players and one player can be in multiple gathers) is implemented using Fluent pivots.

To achieve this, we create a new model class that extends SQLiteUUIDPivot (SQLite is the database we are currently using) and specify the key paths of the tables:

static var leftIDKey: LeftIDKey = PlayerGatherPivot.playerId
static var rightIDKey: RightIDKey = PlayerGatherPivot.gatherId

In our model classes, we can create convenient methods to access the gathers that a player has participated in, and conversely, the players that are part of a given gather:

extension Player {
var gathers: Siblings {
return siblings()
}
}

extension Gather {
var players: Siblings {
return siblings()
}
}

Migrating to Vapor 4

final class PlayerGatherPivot: Model {
static let schema = "player_gather"

@ID(key: .id)
var id: UUID?

@Parent(key: "player_id")
var player: Player

@Parent(key: "gather_id")
var gather: Gather

@Field(key: "team")
var team: String

init() {}

init(playerID: Player.IDValue,
gatherID: Gather.IDValue,
team: String) {
self.$player.id = playerID
self.$gather.id = gatherID
self.team = team
}
}

// MARK: - Migration
extension Gather: Migration {
func prepare(on database: Database) -> EventLoopFuture {
database.schema(Gather.schema)
.field("id", .uuid, .identifier(auto: true))
.field("user_id", .uuid, .required, .references("users", "id"))
.foreignKey("user_id", references: "users", "id", onDelete: .cascade)
.field("score", .string)
.field("winner_team", .string)
.create()
}
}

extension Player: Migration {
func prepare(on database: Database) -> EventLoopFuture {
database.schema(Player.schema)
.field("id", .int, .identifier(auto: true))
.field("user_id", .uuid, .required, .references("users", "id"))
.foreignKey("user_id", references: "users", "id", onDelete: .cascade)
.field("name", .string, .required)
.field("age", .int)
.field("skill", .string)
.field("position", .string)
.field("favourite_team", .string)
.create()
}
}

Controllers

The service logic and routing are managed with RouteCollections.

In the boot function, we define the service paths and specify the methods to handle incoming requests.

For all collections, we define the resource path as api/{resource}. For example: api/users, api/gathers, or api/players.

Thus, if a GET request is made to https://foo.net/api/{resource}, Vapor will look for the corresponding route and method. Remember to register it in routes.swift.

UserController

  • POST /api/users/login — Login functionality for users
  • POST /api/users — Registers a new user
  • GET /api/users — Gets the list of users
  • GET /api/users/{userId} — Gets user by their ID
  • DELETE /api/users/{user_id} — Deletes a user by the given ID

Code snippet:

extension UserController {
func createHandler(_ req: Request) throws -> EventLoopFuture {
let user = try req.content.decode(User.self)
user.password = try Bcrypt.hash(user.password)

return user.save(on: req.db).map {
let response = Response()
response.status = .created

if let userID = user.id?.description {
let location = req.url.path + "/" + userID
response.headers.replaceOrAdd(name: "Location", value: location)
}

return response
}
}
}

Before saving the user in the database, we hash the password using BCryptDigest.

After the user is saved, we retrieve the ID and return it as part of the Location response header, adhering to RESTful API practices by returning status codes instead of the actual resource.

The newly created resource can be associated with the unique ID provided in the Location header field. (https://restfulapi.net/http-status-codes/).

The full implementation can be found on GitHub.

GatherController

The GatherController contains the following methods:

GET /api/gathers — Retrieves all gathers for the authenticated user

POST /api/gathers — Creates a new gather for the authenticated user.

  • The model for creation data is a subset of the actual Gather model, containing the score and the winner team, both parameters being optional.
  • This is similar to the User’s create method; if successful, we return the gather ID as part of the Location header.

DELETE /api/gathers/{gather_id} — Deletes the gather by its ID

PUT /api/gathers/{gather_id} — Updates the gather associated with the given ID.

  • We search through all saved gathers to find a match for the given ID.
  • If successful, we update the score and winner team of the gather.
  • Finally, we return 204.

POST /api/gathers/{gather_id}/players/{player_id} — Adds a player to a gather.

  • Many-to-many relationship.
  • We search for a gather with the given ID, similar to the update method.
  • If found, we retrieve it and create a new pivot.
  • Finally, we save the pivot model in the database and return 200.

GET /api/gathers/{gather_id}/players — Returns the players for the given gather.

  • Similar to the update and add player methods, we search for the gather with the given ID.
  • If found, we return all players associated with that gather.

Code snippet:

func addPlayerHandler(_ req: Request) throws -> EventLoopFuture {
let user = try req.auth.require(User.self)
let playerGatherData = try req.content.decode(PlayerGatherData.self)

guard let gatherID = req.parameters.get("gatherID", as: UUID.self) else {
throw Abort(.badRequest)
}

guard let playerID = req.parameters.get("playerID", as: Int.self) else {
throw Abort(.badRequest)
}

let gather = user.$gathers.query(on: req.db)
.filter(\.$id == gatherID)
.first()

let player = user.$players.query(on: req.db)
.filter(\.$id == playerID)
.first()

return gather.and(player).flatMap { _ in
let pivot = PlayerGatherPivot(playerID: playerID,
gatherID: gatherID,
team: playerGatherData.team)

return pivot.save(on: req.db).transform(to: .ok)
}
}

Conclusion

Vapor is an excellent framework for server-side programming, offering rich APIs for your web applications. More importantly, it is built on top of Swift, integrating the new features of the language into its core. This allows you to harness the true power of Swift, including features like property wrappers.

In this article, we examined a practical example of how to migrate an older application written in Vapor 3 to the latest version, Vapor 4.

The migration wasn’t as straightforward as we expected; many changes occurred between these versions.
The most notable change is the Fluent Models, which have now been moved to their own package in Vapor 4.

For a detailed overview of all changes, you can check this specific commit on GitHub.

--

--

Radu Dan
Radu Dan

Written by Radu Dan

iOS Developer / Doing magic things in Swift

No responses yet