Radu Dan
Vapor and Fluent image

Using Vapor and Fluent to create a REST API

Swift is awesome. Yes, it's mature (now with 5.0 we have ABI stability, hooray!), you have the power of OOP, POP, functional and imperative programming in your hands.

If you want to skip this part, the whole project is found on GitHub https://github.com/radude89/footballgather-ws.

The article is available also on Medium https://medium.com/@radu.ionut.dan/using-vapor-and-fluent-to-create-a-rest-api-5f9a0dcffc7b.

You can do almost anything in Swift nowadays. If you ever thought of being a full stack developer with knowing both backend and frontend, then this article is for you. The most known web frameworks written in Swift are Kitura and Vapor. Vapor is now at version 3 (released in May, 2018), is open source and you can easily create your REST API, web application or your awesome website.

In this tutorial you will learn:

  • how to get started with Vapor
  • create your first REST API
  • how to use Fluent ORM Framework
  • how to transform 1:M and M:M db relationships to parent-child or siblings relationships in Fluent
  • apply what you learn in a real scenario example

Prerequisites

  • Xcode 10.2
  • Knowledge of Swift
  • Basic knowledge of REST API
  • Some knowledge of Swift Package Manager

Getting Started

First, you need to install Xcode from Mac App Store.
You can use brew to install Vapor Toolbox. This is useful so we can run command line tasks for common operations.

/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"

brew tap vapor/tap

brew install vapor/tap/vapor

Go to your projects folder and run command vapor new FootballGatherWS.

You are ready to go!

Football Gather

iOS App Example

FootballGather is a demo project for friends to get together and play football matches as quick as possible. You can imagine the client app by looking at this mockups (created with Balsamiq):

Football Gather Mockups

Features:

  • Ability to add players
  • Set countdown timer for matches
  • Use the application in offline mode
  • Persist players

Database Structure

Let's use a database schema like in the image below:

Football Gather DB Diagram

In this way we can exemplify the 1:M relationship between users and gathers, where one user can create multiple gathers and M:M Player to Gather, where a gather can have multiple players and a player can play in multiple gathers.

List of Controllers

If we look at the iOS app, we will create the following controllers:

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

PlayerController

  • GET /api/players — Gets the list of players
  • GET /api/players/{playerId} — Gets the player by its id
  • GET /api/players/{playerId}/gathers — Gets the list of gathers for the player
  • POST /api/players — Adds a new player
  • DELETE /api/players/{playerId} — Deletes a player with a given id
  • PUT /api/players/{playerId} — Updates a player by its id

GatherController

  • GET /api/gathers — Gets the list of gathers
  • GET /api/gathers/{gatherId} — Gets the gather by its id
  • GET /api/gathers/{gatherId}/players — Gets the list of players in the gather specified by id
  • POST /api/gathers/{gatherId}/players/{playerId} — Adds a player to the gather
  • POST /api/gathers — Adds a new gather
  • DELETE /api/gathers/{gatherId} — Deletes a gather with a given id
  • PUT /api/gathers/{gatherId} — Updates a gather by its id

App Structure

Open the Xcode project that you created in previous section. Type vapor xcode -y.
This may take a while.

Here are the generated files:

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

What you will be touching in this project:

File Description
Package.swift This is the manifest of the project and defines all dependencies and the targets of our app.
I am using Vapor 3.3.0. You can change Package.swift as below:
.package(url: "https://github.com/vapor/vapor.git", from: "3.3.0")
Public All the resources that you want to make them 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: User, Player, Gather.
Controllers The controller is where you write the logic of your REST API, such as CRUD operations.
Similar with 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.

Implementing the User Controller

Before starting to implement our user controller, remove the generated Todo related code:

  • TodoController.swift from controllers folder.
  • Todo.swift from Models.
  • Line migrations.add(model: Todo.self, database: .sqlite) from configure.swift
  • All that is found in routes function from routes.swift.

A user will be defined by a username and a password. The primary key will be of type UUID representing a unique String.

Create a new file, User.swift and add it to the Models folder. Add in the file a class called User.

Make it comply to the following protocols:

  • Codable: Map the parameters of the service to the actual class parameters.
  • SQLiteUUIDModel: Convenience helper protocol to make the Model as an SQLite Model class with a UUID as primary key. Used for< compilation safety for referring to properties.
  • Content: Used to easy decode the information with Vapor.
  • Migration: Tells Fluent how to configure the database.
  • Parameter: Used for requests with parameters, such as GET /users/{userId}
  • .

Now you have your User model. Let's create the UserController.

Create a new struct called UserController inside Controllers folder. Make it comply to RouteCollection protocol.
Leave the boot function for now.

Next we are going to add the CRUD operations for Users.

GET all users

            func getAllHandler(_ req: Request) throws -> Future<[User]> {
return User.query(on: req).decode(data: User.self).all()
}
          

This will return all of the users in the database by using a Fluent query. We use Future for async call; we don't know when our data will return.

GET specific user

            func getHandler(_ req: Request) throws -> Future<User> {
return try req.parameters.next(User.self)
}
      

This will extract the user id from the request and query the database to return the User.

CREATE user

        func createHandler(_ req: Request, user: User) throws -> Future<Response> {
return user.save(on: req).map { user in
var httpResponse = HTTPResponse()
httpResponse.status = .created

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

let response = Response(http: httpResponse, using: req)
return response
}
}
      

We are going to use save function that returns a user object. Following REST API standard, we extract the created UUID of the user and return as part of the Location header of the response.

DELETE user

        func deleteHandler(_ req: Request) throws -> Future<HTTPStatus> {
return try req.parameters.next(User.self).flatMap(to: HTTPStatus.self) { user in
return user.delete(on: req).transform(to: .noContent)
}
}

      

First we extract the user id from the request parameters. We perform delete function on the user and return a HTTPStatus associated with no content.

Implementing the PlayerController

PlayerController follows the same pattern as UserController. The extra function in this case consists of update part.

      func updateHandler(_ req: Request) throws -> Future<HTTPStatus> {
return try flatMap(to: HTTPStatus.self, req.parameters.next(Player.self), req.content.decode(Player.self)) { player, updatedPlayer in
player.age = updatedPlayer.age
player.name = updatedPlayer.name
player.preferredPosition = updatedPlayer.preferredPosition
player.favouriteTeam = updatedPlayer.favouriteTeam
player.skill = updatedPlayer.skill

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

If we look at this function, we first extract the player id for the player that we want to perform an update. Next, we extract all of the properties and map them to a Player object. We perform the update and as you might guessed it we call save method.

Register functions

At the end, make sure you register your handlers inside the boot function:

func boot(router: Router) throws {
let playerRoute = router.grouped("api", "players")
playerRoute.get(use: getAllHandler)
playerRoute.get(Player.parameter, use: getHandler)
playerRoute.post(Player.self, use: createHandler)
playerRoute.delete(Player.parameter, use: deleteHandler)
playerRoute.put(Player.parameter, use: updateHandler)
playerRoute.get(Player.parameter, "gathers", use: getGathersHandler)
}
    

Implementing the GatherController

We stick to the same pattern as for UserController.

1:M and M:M relationships

In order to implement a relationship between two model classes we will have to create a Pivot class.


    final class PlayerGatherPivot: SQLiteUUIDPivot {
var id: UUID?
var playerId: Player.ID
var gatherId: Gather.ID
var team: String

typealias Left = Player
typealias Right = Gather

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

init(playerId: Player.ID, gatherId: Gather.ID, team: String) {
self.playerId = playerId
self.gatherId = gatherId
self.team = team
}
}

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

// Gather.swift
extension Gather {
var players: Siblings<Gather, Player, PlayerGatherPivot> {
return siblings()
}
}
    

The implementation from above describes the M:M relationship between players and gathers. We use the left key as the primary key for players table and the right key as the primary key for gathers. This is similar as a primary key composed of FK/PK for a M:M relationship. The 'team' attribute describes the team in which the player is member in the current gather. We will have to specify the siblings inside our model classes. This is done using Generic principle from Swift.

For a 1:M relationship we can look at User v Gather:

            final class Gather: Codable {
var userId: User.ID
...
}

extension Gather {
var user: Parent<Gather, User> {
return parent(\.userId)
}
}
  

Inside our controller classes the methods can be seen below:

    extension GatherController {
func getPlayersHandler(_ req: Request) throws -> Future<[Player]> {
return try req.parameters.next(Gather.self).flatMap(to: [Player].self) { gather in
return try gather.players.query(on: req).all()
}
}
}

extension PlayerController {
func getGathersHandler(_ req: Request) throws -> <[Gather]> {
return try req.parameters.next(Player.self).flatMap(to: [Gather].self) { player in
return try player.gathers.query(on: req).all()
}
}
}

  

Registering routes and configuring database

Open routes.swift and add the following inside routes function

    let userController = UserController()
try router.register(collection: userController)

let playerController = PlayerController()
try router.register(collection: playerController)

let gatherController = GatherController()
try router.register(collection: gatherController)
  

These lines will register all of your controllers.

In configure.swift add all of your models in the MigrationsConfig:

            migrations.add(model: User.self, database: .sqlite)
migrations.add(model: Player.self, database: .sqlite)
migrations.add(model: Gather.self, database: .sqlite)
migrations.add(model: PlayerGatherPivot.self, database: .sqlite)
  

That's it. Build & run.