Radu Dan
Vapor 4 Banner

Migrating to Vapor 4

In this article we are going to see how we can migrate a web application, developed in Vapor 3 to the newest version, Vapor 4.

Short recap

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

The server side app structure in Vapor 3:

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

File Description
Package.swift This is the manifest of the project and defines all dependencies and targets of our app.
The project is pointing to Vapor 3.3.0:
.package(url: "https://github.com/vapor/vapor.git", from: "3.3.0")
Public All resources you want to be public, such as images.
Source Here you can see two separate modules: App and Run.
You usually have to put all of your developed code inside App.
The Run folder contains the main.swift file.
Models Add here your Fluent models. In our app, the models are: User, Player, Gather.
Controllers The controller is where you write the logic of your REST API, such as CRUD operations.
These are similar to iOS ViewControllers, but instead they handle the requests and manage the models.
routes.swift Used to find the appropriate response for an incoming request.
configure.swift Called before app is initialised. Register router, middlewares, database and model migrations.

When the server app runs it goes to:
main.swift >>> app.swift (to create an instance of the app) >>> configure.swift (called before the app initializes) >>> routes.swift (checks the registered routes) >>> boot.swift (called after the app is initialized).

Migration

Package

A lot of things changed from Vapor 3 and migrating to Vapor 4 is not as straight forward as you might think.

Next, we present the differences of Package.swift. You can check it also on GitHub.

Vapor 4 diffs

Updating our Models

Vapor 4 uses the true power of Swift 5.2 and has property wrappers implemented in its core.
The Model protocol now replaces every SQLiteTypeModel that we had to extend in v3.
We no longer have to implement Migration in our Model classes.

We remove the SQLiteUUIDPivot protocol implementation and make the PlayerGatherPivot implement the default Model protocol.

Foreign/Private keys of the table are specified using the @Parent property wrapper.

Example of an implemented model, PlayerGatherPivot:


    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<Void> {
            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<Void> {
            database.schema(PlayerGatherPivot.schema).delete()
        }
    }
  

Updating our Controllers

Route collections use the same function boot, but the function's parameter type has been changed from Router to RoutesBuilder.

We don’t use Model.parameter anymore. Instead, we use the special mark :id.

Vapor 4 diffs

Fetching the authenticated user has been replaced from (Vapor 3):

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

To (in Vapor 4):

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

Speaking about authentication, the middlewares are now updated to:

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

The token auth middleware is now implementing 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 controllers in GitHub:

The CRUD operations have suffered a lot of transformation:

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
           )}
}

flatMapEachThrowing is used to call a closure on each element in the sequence that is wrapped by an EventLoopFuture.

CREATE a gather

func createHandler(_ req: Request) throws -> EventLoopFuture<Response> {
  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)
       }
   }
}

DELETE a gather

func deleteHandler(_ req: Request) throws -> EventLoopFuture<HTTPStatus> {
   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
       if let gather = gathers.first(where: { $0.id == id }) {
           return gather.delete(on: req.db).transform(to: HTTPStatus.noContent)
       }

       return req.eventLoop.makeFailedFuture(Abort(.notFound))
   }
}

UPDATE a gather

func updateHandler(_ req: Request) throws -> EventLoopFuture<HTTPStatus> {
    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: HTTPStatus.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<HTTPStatus> {
   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: HTTPStatus.ok)
   }
}

Models in Vapor 4

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

User Model

User has three parameters: 

  • id: the primary key as UUID
  • username: a unique name created at registration
  • password: the hashed password of the user

We have an inner class - User.Public - to define what public details (just the username) we expose to methods such as GET.

The 1:M relationships with gathers (one user can create multiple gathers) and with players (one user can create multiple players) are implemented with 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

With Token model, we define a mapping between a generated 16 byte data, base64 encoded (the actual token) and the user ID.

For generating the token, we use CryptoRandom().generateData function that at core uses OpenSSL RAND_bytes to generate random data of specified length.

The authentication pattern for the server app is Bearer token auth.
Vapor is really cool and to use it, we just need to implement BearerAuthenticatable protocol and specify 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

Gather is the model for our football matches.

It has a parent identifier, the user ID and two optional string parameters, the score and the winner 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

Player model is defined similar with Gather:

  • userID: the ID of the user that created this player (the parent)
  • name: combines the first and last names
  • age: an optional integer that you can use to store the age of the player
  • skill: is an enum that specifies what skill the player has (beginner, amateur or professional)
  • preferredPosition: represents the position in the field the player prefers
  • favouriteTeam: an optional string parameter to record the favourite team of the player

    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 M:M to relationship between gathers and players (one gather can have multiple players and one player can be in multiple gathers) is implemented with Fluent pivots.

Basically, we create a new model class that extends SQLiteUUIDPivot (SQLite is the database we are currently using) and we 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 our player has been part of and, respectively, the players that were in the given gather:

extension Player {
    var gathers: Siblings<Player, Gather, PlayerGatherPivot> {
        return siblings()
    }
}


extension Gather {
    var players: Siblings<Gather, Player, PlayerGatherPivot> {
        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<Void> {
        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<Void> {
        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 services logic and routing is done with RouteCollections.

In the boot function, we define the service paths and what methods to use for the incoming requests.

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

So, for example, if a GET request is made to https://foo.net/api/{resource}, Vapor will search for the route and method. Don’t forget 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 — Get the list of users
  • GET /api/users/{userId} — Get user by its id
  • DELETE /api/users/{user_id} — Deletes a user by a given ID

Code snippet:

extension UserController {
    func createHandler(_ req: Request) throws -> EventLoopFuture<Response> {
        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 with BCryptDigest.

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

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

Full implementation can be found on GitHub.

GatherController

In GatherController you can find the following methods:

  • GET /api/gathers — Gets all gathers for the authenticated user
  • POST /api/gathers — Creates a new gather for the authenticated user.
    • The model for create data is a subset of the actual Gather model, containing the score and the winner team, both parameters being optional.
    • This is similar with 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 via its ID
  • PUT /api/gathers/{gather_id} — Updates the gather associated with the given ID.
    • We look in all saved gathers trying to match the given ID with one of the existing ones.
    • If success, we update the score and winner team of the gather.
    • In the end, we return 204.
  • POST /api/gathers/{gather_id}/players/{player_id} — Adds a player to a gather.
    • M:M relationship
    • We search in all gathers for a gather that has the given ID, same as for the update method
    • If we find a gather, we retrieve it and create a new pivot
    • In the end, 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 with update and adding a player to a gather methods, we search for the gather that has the given ID
    • If found, we return all players associated with that gather

Code snippet:

func addPlayerHandler(_ req: Request) throws -> EventLoopFuture<HTTPStatus> {
   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: HTTPStatus.ok)
   }
}

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
               )}
   }
}

PlayerController

The methods defined in PlayerController can be found below:

  • GET /api/players — Retrieves all players for the authenticated user
  • POST /api/players — Creates a new player for the authenticated user
  • DELETE /api/players/{player_id} — Deletes the player if is found by the given ID
  • PUT /api/players/{player_id} — Updates a player
    • Similar with Gather and User collection, we look into all players for a match of the ID
    • If successful, we update all player parameters (age, name, preferred position, favourite team, skill)
    • In the end we return 204, NO_CONTENT.
  • GET /api/players/{player_id}/gathers — Returns all gathers for the player matching the given ID.
    • Follows the same pattern as we have for gathers where we can return the players for the given gather ID

Code snippet:

func getGathersHandler(_ req: Request) throws -> EventLoopFuture<[GatherResponseData]> {
   let user = try req.auth.require(User.self)

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

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

       return player.$gathers.query(on: req.db)
           .all()
           .flatMapEachThrowing {
               try GatherResponseData(
                   id: $0.requireID(),
                   userId: user.requireID(),
                   score: $0.score,
                   winnerTeam: $0.winnerTeam
               )}
   }
}

Conclusion

Vapor is a great framework for server side programming, offering rich APIs for your web app. More important, is built on top of Swift 5.2, having the newly addons of the language integrated in its core. This offers you the true power of Swift, such as property wrappers.

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

The migration wasn't as straight forward as we would expected. A lot of things changed between these versions.
The most notable change are the Fluent Models. Fluent got its own package in Vapor 4.

For all changes, you can check this particular commit on GitHub.

Useful Links