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.
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
If you want to skip this part, the whole project is found on GitHub:
Prerequisites
For this tutorial you will need:
- 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
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:
- Persist players
- 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 — Get the list of users
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:
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 UserController
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 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 = .createdif 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 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.skillreturn 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
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)
}
For 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: Stringtypealias Left = Player
typealias Right = Gatherstatic var leftIDKey: LeftIDKey = \PlayerGatherPivot.playerId
static var rightIDKey: RightIDKey = \PlayerGatherPivot.gatherIdinit(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 -> Future<[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.