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):
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:
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:
├── 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)
fromconfigure.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 asGET /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.