Vapor 4 — Server Side Swift

Migrating to Vapor 4

Radu Dan
7 min readNov 10, 2020

--

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. This article can also be found on my personal website.

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

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 the 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.

Code diffs of Package.swift between Vapor 3 and 4

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:

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.

GatherController differences between Vapor 3 and 4

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

To (in Vapor 4):

Speaking about authentication, the middlewares are now updated to:

The token auth middleware is now implementing a new protocol, called ModelTokenAuthenticatable:

You can check all Vapor 4 controllers in GitHub:

The CRUD operations have suffered a lot of transformation:

GET all gathers

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

CREATE a gather

DELETE a gather

UPDATE a gather

GET players of a specified gather

POST player to a specified gather

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:

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

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.

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.

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

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:

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:

Migrating to Vapor 4

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

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:

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:

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 add-ons 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.

--

--