Server-Side Swift With Vapor (Third Edition)
Server-Side Swift With Vapor (Third Edition)
Swift
with
Vapor
By Tim Condon, Tanner Nelson & Logan Wright
Licensing
Server-Side Swift with Vapor
By Tim Condon, Tanner Nelson & Logan Wright
Notice of Rights
All rights reserved. No part of this book or
corresponding materials (such as text, images,
or source code) may be reproduced or
distributed by any means without prior written
permission of the copyright owner.
Notice of Liability
This book and all corresponding materials (such
as source code) are provided on an “as is” basis,
without warranty of any kind, express of
implied, including but not limited to the
warranties of merchantability, fitness for a
particular purpose, and noninfringement. In no
event shall the authors or copyright holders be
liable for any claim, damages or other liability,
whether in action of contract, tort or otherwise,
arising from, out of or in connection with the
software or the use of other dealing in the
software.
Trademarks
All trademarks and registered trademarks
appearing in this book are the property of their
own respective owners.
About the Authors
— Tim Condon
www.raywenderlich.com/newsletter
Before You Begin
This section tells you a few things you need to
know before you get started, such as what you’ll
need for hardware and software, where to find
the project files for this book, and more.
What You Need
To follow along with this book, you’ll need the
following:
https://github.com/raywenderlich/vpr-
materials/tree/editions/3.0
Forums
We’ve also set up an official forum for the book
at forums.raywenderlich.com. This is a great
place to ask questions about the book or to
submit any errors you may find.
About the Cover
http://www.iucnredlist.org/details/1095/0
http://www.pbs.org/wgbh/nova/next/nature
/saving-axolotls/
Section I: Creating a
Simple Web API
This section teaches you the beginnings of
building Vapor applications, including how to
use Swift Package Manager. You’ll learn how
routing works and how Vapor leverages the
power of Swift to make routing type-safe. You’ll
learn how to create models, set up relationships
between them and save them in a database.
You’ll see how to provide an API to access this
data from a REST client. Finally, you’ll build an
iOS app which leverages this API to allow users
to display and interact with the data.
Chapter 1: Introduction
Vapor is an open-source web framework written
in Swift. It’s built on top of Apple’s SwiftNIO
library to provide a powerful, asynchronous
framework. Vapor allows you to build back-end
applications for iOS apps, front-end web sites
and stand-alone server applications.
About Vapor
Apple open-sourced Swift in December 2015,
thereby enabling developers to create
applications for macOS and Linux written in
Swift. Almost immediately, a number of web
frameworks written in Swift appeared. Tanner
Nelson started Vapor in January 2016, and
Logan Wright joined him shortly thereafter.
Over time, a large and engaged user community
has embraced the framework. Vapor has a Swift-
like API and makes heavy use of many powerful
language features. As a result, it has become the
most popular server-side Swift framework on
GitHub.
Vapor consists of a number of packages
including Leaf — a templating engine for front-
end development — and Fluent, a Swift Object
Relational Mapping (ORM) framework with
native, asynchronous database drivers. One of
its biggest strengths is its community. There’s a
very dedicated following on GitHub and an
extremely active chat server on Discord.
Update Note
The third edition of this book is a complete
rewrite to update it for Vapor 4! This edition
also includes a new chapter on how to
implement Sign In With Apple.
Chapter 2: Hello,
Vapor!
Beginning a project using a new technology can
be daunting. Vapor makes it easy to get started.
It provides a handy command line tool to create
a starter project for you.
Vapor Toolbox
The Vapor Toolbox is a command line interface
(CLI) tool you use when developing Vapor apps.
It helps you create a new Vapor project from a
template and can add dependencies as needed.
Installing on macOS
Vapor uses Homebrew to install the Toolbox.
Installing on Linux
This book focuses primarily on using Xcode and
macOS for developing your apps. However,
everything you build with Vapor will work on
versions of Linux that Swift supports. The Vapor
Toolbox works in exactly the same way, with the
exception that you can’t use Xcode on Linux.
Installing Swift
To install Swift on Linux, go to
https://swift.org/download/ and download the
toolchain for your operating system. Follow the
installation to install the toolchain on your
machine. When complete, enter the following at
a shell prompt:
swift --version
Installing Vapor
In your console, run the following commands:
# 1
git clone https://github.com/vapor/toolbox.g
it
# 2
cd toolbox
# 3
git checkout 18.0.0
# 4
swift build -c release --disable-sandbox
# 5
mv .build/release/vapor /usr/local/bin
mkdir ~/vapor
cd ~/vapor
open .
Notice there’s no Xcode project in your template
even though you’ve built and run the app. This
is deliberate. In fact, the project file is explicitly
excluded from source control using the
.gitignore file. When using SwiftPM, Xcode
creates a workspace in a hidden directory called
.swiftpm.
open Package.swift
URL: http://localhost:8080/info
Method: POST
// 1
app.post("info") { req -> String in
let data = try req.content.decode(InfoDat
a.self)
return "Hello \(data.name)!"
}
Returning JSON
Vapor also makes it easy to return JSON in your
route handlers. This is a common need when
your app provides an API service. For example, a
Vapor app that processes requests from an iOS
app needs to send JSON responses. Vapor again
uses Content to encode the response as JSON.
// 1
app.post("info") { req -> InfoResponse in
let data = try req.content.decode(InfoDat
a.self)
// 2
return InfoResponse(request: data)
}
HTTP requests
An HTTP request consists of several parts:
GET
HEAD
POST
PUT
DELETE
CONNECT
OPTIONS
TRACE
PATCH
HTTP responses
The server returns an HTTP response when it
has processed a request. An HTTP response
consists of:
Response headers
HTTP 2.0
Most web services today use HTTP version 1.1 —
released in January 1997 as RFC 2068.
Everything you’ve learned so far is part of
HTTP/1.1 and, unless otherwise noted, is the
version used throughout this book.
REST
REST, or representational state transfer, is an
architectural standard closely related to HTTP.
Many APIs used by apps are REST APIs and
you’ll hear the term often. You’ll learn more
about REST and how it relates to HTTP and
CRUD in Chapter 7: “CRUD Database
Operations”. REST provides a way of defining a
common standard for accessing resources from
an API. For example, for an acronyms API, you
might define the following endpoints:
Async
One of Vapor’s most important features is Async.
It can also be one of the most confusing. Why is
it important?
For example:
// 1
return database.getAllUsers().flatMap { users
in
// 2
let user = users[0]
user.name = "Bob"
// 3
return user.save(on: req.db).map { user in
//4
return .noContent
}
}
Flatten
There are times when you must wait for a
number of futures to complete. One example
occurs when you’re saving multiple models in a
database. In this case, you use flatten(on:). For
instance:
Multiple futures
Occasionally, you need to wait for a number of
futures of different types that don’t rely on one
another. For example, you might encounter this
situation when retrieving users from the
database and making a request to an external
API. SwiftNIO provides a number of methods to
allow waiting for different futures together. This
helps avoid deeply nested code or confusing
chains.
// 1
getAllUsers()
// 2
.and(req.client.get("http://localhost:808
0/getUserData"))
// 3
.flatMap { users, response in
// 4
users[0].addData(response).transform(to:
.noContent)
}
// 1
getAllUsers()
// 2
.and(req.client.get("http://localhost:808
0/getUserData"))
// 3
.map { users, response in
// 4
users[0].syncAddData(response)
// 5
return .content
}
5. Return .noContent.
getAllUsers()
.and(getAllAcronyms())
.and(getAllCategories()).flatMap { result i
n
// Use the different futures
}
result is of type (([User], [Acronyms]),
[Categories]). And the more futures you chain
with and(_:), the more nested tuples you get.
This can get a bit confusing! :]
Creating futures
Sometimes you need to create your own futures.
If an if statement returns a non-future and the
else block returns an EventLoopFuture, the
compiler will complain that these must be the
same type. To fix this, you must convert the
non-future into an EventLoopFuture using
request.eventLoop.future(_:). For example:
// 1
func createTrackingSession(for request: Requ
est)
-> EventLoopFuture<TrackingSession> {
return request.makeNewSession()
}
// 2
func getTrackingSession(for request: Reques
t)
-> EventLoopFuture<TrackingSession> {
// 3
let session: TrackingSession? =
TrackingSession(id: request.getKey())
// 4
guard let createdSession = session else {
return createTrackingSession(for: reques
t)
}
// 5
return request.eventLoop.future(createdSes
sion)
}
5. Create an EventLoopFuture<TrackingSession>
from createdSession using
request.eventLoop.future(_:). This returns
the future on the request’s EventLoop.
For example:
// 1
req.client.get("http://localhost:8080/user
s")
.flatMapThrowing { response in
// 2
let users = try response.content.decode([U
ser].self)
// 3
return users[0]
}
Chaining futures
Dealing with futures can sometimes seem
overwhelming. It’s easy to end up with code
that’s nested multiple levels deep.
return database
.getAllUsers()
.flatMap { users in
let user = users[0]
user.name = "Bob"
return user.save(on: req.db)
.map { user in
return .noContent
}
}
return database
.getAllUsers()
// 1
.flatMap { users in
let user = users[0]
user.name = "Bob"
return user.save(on: req.db)
// 2
}.map { user in
return .noContent
}
Always
Sometimes you want to execute something no
matter the outcome of a future. You may need to
close connections, trigger a notification or just
log that the future has executed. For this, use
the always callback.
For example:
// 1
let userResult: EventLoopFuture<User> = use
r.save(on: req.db)
// 2
userResult.always {
// 3
print("User save has been attempted")
}
Here’s what this does:
Waiting
In certain circumstances, you may want to
actually wait for the result to return. To do this,
use wait().
Note: There’s a large caveat around this:
You can’t use wait() on the main event
loop, which means all request handlers and
most other circumstances.
Fluent
Fluent is Vapor’s ORM or object relational
mapping tool. It’s an abstraction layer between
the Vapor application and the database, and it’s
designed to make working with databases easier.
Using an ORM such as Fluent has a number of
benefits.
Acronyms
Over the next several chapters, you’ll build a
complex “Today I Learned” application that can
save different acronyms and their meanings.
Start by creating a new project, using the Vapor
Toolbox. In Terminal, enter the following
command:
cd ~/vapor
cd TILApp
rm -rf Sources/App/Models/*
rm -rf Sources/App/Migrations/*
rm -rf Sources/App/Controllers/*
open Package.swift
app.migrations.add(CreateTodo())
// 1
final class Acronym: Model {
// 2
static let schema = "acronyms"
// 3
@ID
var id: UUID?
// 4
@Field(key: "short")
var short: String
@Field(key: "long")
var long: String
// 5
init() {}
// 6
init(id: UUID? = nil, short: String, long:
String) {
self.id = id
self.short = short
self.long = long
}
}
// 1
struct CreateAcronym: Migration {
// 2
func prepare(on database: Database) -> Eve
ntLoopFuture<Void> {
// 3
database.schema("acronyms")
// 4
.id()
// 5
.field("short", .string, .required)
.field("long", .string, .required)
// 6
.create()
}
// 7
func revert(on database: Database) -> Even
tLoopFuture<Void> {
database.schema("acronyms").delete()
}
}
// 1
app.migrations.add(CreateAcronym())
// 2
app.logger.logLevel = .debug
// 3
try app.autoMigrate().wait()
docker ps
Now you’re ready to run the app! Set the active
scheme to TILApp with My Mac as the
destination. Build and run. Check the console
and see that the migrations have run.
// 1
app.post("api", "acronyms") { req -> EventLo
opFuture<Acronym> in
// 2
let acronym = try req.content.decode(Acron
ym.self)
// 3
return acronym.save(on: req.db).map {
// 4
acronym
}
}
method: POST
short: OMG
long: Oh My God
Choosing a database
Vapor has official, Swift-native drivers for:
SQLite
MySQL
PostgreSQL
MongoDB
There are two types of databases: relational, or
SQL databases, and non-relational, or NoSQL
databases. Relational databases store their data
in structured tables with defined columns. They
are efficient at storing and querying data whose
structure is known up front. You create and
query tables with a structured query language
(SQL) that allows you to retrieve data from
multiple, related tables. For example, if you have
a list of pets in one table and list of owners in
another, you can retrieve a list of pets with their
owners’ names with a single query.
SQLite
SQLite is a simple, file-based relational database
system. It’s designed to be embedded into an
application and is useful for single-process
applications such as iOS applications. It relies
on file locks to maintain database integrity, so
it’s not suitable for write-intensive applications.
This also means you can’t use it across servers.
It is, however, a good database for both testing
and prototyping applications.
MySQL
MySQL is another open-source, relational
database made popular by the LAMP web
application stack (Linux, Apache, MySQL, PHP).
It’s become the most popular database due to its
ease of use and support from most cloud
providers and website builders.
PostgreSQL
PostgreSQL — frequently shortened to Postgres
— is an open-source, relational database system
focused on extensibility and standards and is
designed for enterprise use. Postgres also has
native support for geometric primitives, such as
coordinates. Fluent supports these primitives as
well as saving nested types, such as dictionaries,
directly into Postgres.
MongoDB
MongoDB is a popular open-source, document-
based, non-relational database designed to
process large amounts of unstructured data and
to be extremely scalable. It stores its data in
JSON-like documents in human readable
formats that do not require any particular
structure.
Configuring Vapor
Configuring your Vapor application to use a
database follows the same steps for all
supported databases as shown below.
import PackageDescription
import Fluent
// 1
import FluentSQLiteDriver
import Vapor
app.migrations.add(CreateAcronym())
app.logger.logLevel = .debug
try app.autoMigrate().wait()
// register routes
try routes(app)
}
1. Import FluentSQLiteDriver.
2. Configure the application to use an in-
memory SQLite database with the .sqlite
identifier.
app.databases.use(.sqlite(.file("db.sqlit
e")), as: .sqlite)
app.migrations.add(CreateAcronym())
app.logger.logLevel = .debug
try app.autoMigrate().wait()
// register routes
try routes(app)
}
1. Import FluentMySQLDriver.
docker ps
Now that MongoDB is running, set up your
Vapor application. Open Package.swift; replace
its contents with the following:
// swift-tools-version:5.2
import PackageDescription
import Fluent
// 1
import FluentMongoDriver
import Vapor
app.migrations.add(CreateAcronym())
app.logger.logLevel = .debug
try app.autoMigrate().wait()
// register routes
try routes(app)
}
1. Import FluentMongoDriver.
2. Register the database with the application
using the .mongo identifier. MongoDB uses a
connection URL as shown here. The URL
specifies the host — in this case localhost —
the port and the path to the database. The
path is the same as the database name
provided to Docker. By default, MongoDB
doesn’t require authentication, but you
would provide it here if needed.
docker ps
// 3
app.migrations.add(CreateAcronym())
app.logger.logLevel = .debug
// 4
try app.autoMigrate().wait()
// register routes
try routes(app)
}
1. Import FluentPostgresDriver.
When you run your app for the first time, you’ll
see the migrations run:
Where to go from here?
In this chapter, you’ve learned how to configure
a database for your application. The next
chapter introduces CRUD operations so you can
create, retrieve, update and delete your
acronyms.
Chapter 7: CRUD
Database Operations
Chapter 5, “Fluent & Persisting Models”,
explained the concept of models and how to
store them in a database using Fluent. This
chapter concentrates on how to interact with
models in the database. You’ll learn about CRUD
operations and how they relate to REST APIs.
You’ll also see how to leverage Fluent to
perform complex queries on your models.
GET http://localhost:8080/api/acronyms/:
get all the acronyms.
POST http://localhost:8080/api/acronyms:
create a new acronym.
GET http://localhost:8080/api/acronyms/1:
get the acronym with ID 1.
PUT http://localhost:8080/api/acronyms/1:
update the acronym with ID 1.
DELETE
http://localhost:8080/api/acronyms/1:
delete the acronym with ID 1.
Create
In Chapter 5, “Fluent & Persisting Models”, you
implemented the create route for an Acronym.
You can either continue with your project or
open the TILApp in the starter folder for this
chapter. To recap, you created a new route
handler in routes.swift:
// 1
app.post("api", "acronyms") {
req -> EventLoopFuture<Acronym> in
// 2
let acronym = try req.content.decode(Acron
ym.self)
// 3
return acronym.save(on: req.db).map { acro
nym }
}
URL: http://localhost:8080/api/acronyms/
method: POST
short: OMG
long: Oh My God
// 1
app.get("api", "acronyms") {
req -> EventLoopFuture<[Acronym]> in
// 2
Acronym.query(on: req.db).all()
}
URL: http://localhost:8080/api/acronyms/
method: GET
// 1
app.get("api", "acronyms", ":acronymID") {
req -> EventLoopFuture<Acronym> in
// 2
Acronym.find(req.parameters.get("acronymI
D"), on: req.db)
// 3
.unwrap(or: Abort(.notFound))
}
URL:
http://localhost:8080/api/acronyms/<ID>
(replace the ID with the ID of the acronym
you created earlier)
method: GET
Send the request and you’ll receive the first
acronym as the response:
Update
In RESTful APIs, updates to single resources use
a PUT request with the request data containing
the new information.
URL: http://localhost:8080/api/acronyms/
method: POST
method: PUT
Delete
To delete a model in a RESTful API, you send a
DELETE request to the resource. Add the
following to the end of routes(_:) to create a
new route handler:
// 1
app.delete("api", "acronyms", ":acronymID")
{
req -> EventLoopFuture<HTTPStatus> in
// 2
Acronym.find(req.parameters.get("acronymI
D"), on: req.db)
.unwrap(or: Abort(.notFound))
// 3
.flatMap { acronym in
// 4
acronym.delete(on: req.db)
// 5
.transform(to: .noContent)
}
}
URL:
http://localhost:8080/api/acronyms/<ID>
method: DELETE
Filter
Search functionality is a common feature in
applications. If you want to search all the
acronyms in the database, Fluent makes this
easy. Ensure the following line of code is at the
top of routes.swift:
import Fluent
// 1
app.get("api", "acronyms", "search") {
req -> EventLoopFuture<[Acronym]> in
// 2
guard let searchTerm =
req.query[String.self, at: "term"] else
{
throw Abort(.badRequest)
}
// 3
return Acronym.query(on: req.db)
.filter(\.$short == searchTerm)
.all()
}
URL:
http://localhost:8080/api/acronyms/search?
term=OMG
method: GET
// 1
return Acronym.query(on: req.db).group(.or)
{ or in
// 2
or.filter(\.$short == searchTerm)
// 3
or.filter(\.$long == searchTerm)
// 4
}.all()
// 1
app.get("api", "acronyms", "first") {
req -> EventLoopFuture<Acronym> in
// 2
Acronym.query(on: req.db)
.first()
.unwrap(or: Abort(.notFound))
}
short: IKR
URL:
http://localhost:8080/api/acronyms/first
method: GET
method: GET
Controllers
Controllers in Vapor serve a similar purpose to
controllers in iOS. They handle interactions
from a client, such as requests, process them
and return the response. Controllers provide a
way to better organize your code. It’s good
practice to have all interactions with a model in
a dedicated controller. For example in the TIL
application, an acronym controller can handle
all CRUD operations on an acronym.
Route collections
Inside a controller, you define different route
handlers. To access these routes, you must
register these handlers with the router. A simple
way to do this is to call the functions inside your
controller from routes.swift. For example:
app.get(
"api",
"acronyms",
use: acronymsController.getAllHandler)
app.get("api", "acronyms") {
req -> EventLoopFuture<[Acronym]> in
Acronym.query(on: req.db).all()
}
// 1
let acronymsController = AcronymsController
()
// 2
try app.register(collection: acronymsControl
ler)
URL: http://localhost:8080/api/acronyms/
method: GET
app.post("api", "acronyms") {
req -> EventLoopFuture<Acronym> in
let acronym = try req.content.decode(Acron
ym.self)
return acronym.save(on: req.db).map { acro
nym }
}
acronymsRoutes.get(use: getAllHandler)
router.post("api", "acronyms")
router.get("api", "acronyms",
Acronym.parameter)
router.put("api", "acronyms",
Acronym.parameter)
router.delete("api", "acronyms",
Acronym.parameter)
URL:
http://localhost:8080/api/acronyms/first
method: GET
Send the request and you’ll see a previously
created acronym using the new controller:
Parent-child relationships
Parent-child relationships describe a
relationship where one model has “ownership”
of one or more models. They are also known as
one-to-one and one-to-many relationships.
Creating a user
In Xcode, create a new file for the User class
called User.swift in Sources/App/Models. Next,
create a migration file, CreateUser.swift, in
Sources/App/Migrations. Finally, create a file
called UsersController.swift in
Sources/App/Controllers for the UsersController.
User model
In Xcode, open User.swift and create a basic
model for the user:
import Fluent
import Vapor
@ID
var id: UUID?
@Field(key: "name")
var name: String
@Field(key: "username")
var username: String
init() {}
import Fluent
// 1
struct CreateUser: Migration {
// 2
func prepare(on database: Database) -> Eve
ntLoopFuture<Void> {
// 3
database.schema("users")
// 4
.id()
// 5
.field("name", .string, .required)
.field("username", .string, .required)
// 6
.create()
}
// 7
func revert(on database: Database) -> Even
tLoopFuture<Void> {
database.schema("users").delete()
}
}
app.migrations.add(CreateUser())
User controller
Open UsersController.swift and create a new
controller that can create users:
import Vapor
// 1
struct UsersController: RouteCollection {
// 2
func boot(routes: RoutesBuilder) throws {
// 3
let usersRoute = routes.grouped("api",
"users")
// 4
usersRoute.post(use: createHandler)
}
// 5
func createHandler(_ req: Request)
throws -> EventLoopFuture<User> {
// 6
let user = try req.content.decode(User.s
elf)
// 7
return user.save(on: req.db).map { user
}
}
}
// 1
let usersController = UsersController()
// 2
try app.register(collection: usersControlle
r)
Here’s what this does:
// 1
func getAllHandler(_ req: Request)
-> EventLoopFuture<[User]> {
// 2
User.query(on: req.db).all()
}
// 3
func getHandler(_ req: Request)
-> EventLoopFuture<User> {
// 4
User.find(req.parameters.get("userID"), on:
req.db)
.unwrap(or: Abort(.notFound))
}
// 1
usersRoute.get(use: getAllHandler)
// 2
usersRoute.get(":userID", use: getHandler)
URL: http://localhost:8080/api/users
method: POST
@Parent(key: "userID")
var user: User
// 1
init(
id: UUID? = nil,
short: String,
long: String,
userID: User.IDValue
) {
self.id = id
self.short = short
self.long = long
// 2
self.$user.id = userID
}
This adds the new column for user using the key
provided to the @Parent property wrapper. The
column type, uuid, matches the ID column type
from CreateUser.
{
"short": "OMG",
"long": "Oh My God",
"user": {
"id": "2074AD1A-21DC-4238-B3ED-D076BBE5D
135"
}
}
{
"short": "OMG",
"long": "Oh My God",
"userID": "2074AD1A-21DC-4238-B3ED-D076BBE
5D135"
}
URL: http://localhost:8080/api/acronyms
method: POST
short: OMG
long: Oh My God
userID: the ID you copied earlier
// 1
func getUserHandler(_ req: Request)
-> EventLoopFuture<User> {
// 2
Acronym.find(req.parameters.get("acronymI
D"), on: req.db)
.unwrap(or: Abort(.notFound))
.flatMap { acronym in
// 3
acronym.$user.get(on: req.db)
}
}
acronymsRoutes.get(":acronymID", "user", us
e: getUserHandler)
URL:
http://localhost:8080/api/acronyms/<ID of
your acronym>/user
method: GET
@Children(for: \.$user)
var acronyms: [Acronym]
// 1
func getAcronymsHandler(_ req: Request)
-> EventLoopFuture<[Acronym]> {
// 2
User.find(req.parameters.get("userID"), on:
req.db)
.unwrap(or: Abort(.notFound))
.flatMap { user in
// 3
user.$acronyms.get(on: req.db)
}
}
usersRoute.get(
":userID",
"acronyms",
use: getAcronymsHandler)
URL: http://localhost:8080/api/users/<ID of
your user>/acronyms
method: GET
app.migrations.add(CreateUser())
app.migrations.add(CreateAcronym())
URL: http://localhost:8080/api/acronyms/
method: POST
short: OMG
long: Oh My God
userID: E92B49F2-F239-41B4-B26D-
85817F0363AB
Sibling relationships
Sibling relationships describe a relationship
that links two models to each other. They are
also known as many-to-many relationships.
Unlike parent-child relationships, there are no
constraints between models in a sibling
relationship.
Creating a category
To implement categories, you’ll need to create a
model, a migration, a controller and a pivot.
Begin by creating the model.
Category model
In Xcode, create a new file Category.swift in
Sources/App/Models. Open the file and insert a
basic model for a category:
import Fluent
import Vapor
@ID
var id: UUID?
@Field(key: "name")
var name: String
init() {}
import Fluent
app.migrations.add(CreateCategory())
This adds the new migration to the application’s
migrations so that Fluent creates the table in
the database at the next application start.
Category controller
Now it’s time to create the controller. In
Sources/App/Controllers, create a new file called
CategoriesController.swift. Open the file and
add code for a new controller to create and
retrieve categories:
import Vapor
// 1
struct CategoriesController: RouteCollection
{
// 2
func boot(routes: RoutesBuilder) throws {
// 3
let categoriesRoute = routes.grouped("ap
i", "categories")
// 4
categoriesRoute.post(use: createHandler)
categoriesRoute.get(use: getAllHandler)
categoriesRoute.get(":categoryID", use:
getHandler)
}
// 5
func createHandler(_ req: Request)
throws -> EventLoopFuture<Category> {
// 6
let category = try req.content.decode(Ca
tegory.self)
return category.save(on: req.db).map { c
ategory }
}
// 7
func getAllHandler(_ req: Request)
-> EventLoopFuture<[Category]> {
// 8
Category.query(on: req.db).all()
}
// 9
func getHandler(_ req: Request)
-> EventLoopFuture<Category> {
// 10
Category.find(req.parameters.get("categor
yID"), on: req.db)
.unwrap(or: Abort(.notFound))
}
}
URL: http://localhost:8080/api/categories
method: POST
name: Teenager
// 1
final class AcronymCategoryPivot: Model {
static let schema = "acronym-category-pivo
t"
// 2
@ID
var id: UUID?
// 3
@Parent(key: "acronymID")
var acronym: Acronym
@Parent(key: "categoryID")
var category: Category
// 4
init() {}
// 5
init(
id: UUID? = nil,
acronym: Acronym,
category: Category
) throws {
self.id = id
self.$acronym.id = try acronym.requireID
()
self.$category.id = try category.require
ID()
}
}
// 1
struct CreateAcronymCategoryPivot: Migration
{
// 2
func prepare(on database: Database) -> Eve
ntLoopFuture<Void> {
// 3
database.schema("acronym-category-pivo
t")
// 4
.id()
// 5
.field("acronymID", .uuid, .required,
.references("acronyms", "id", onDele
te: .cascade))
.field("categoryID", .uuid, .required,
.references("categories", "id", onDe
lete: .cascade))
// 6
.create()
}
// 7
func revert(on database: Database) -> Even
tLoopFuture<Void> {
database.schema("acronym-category-pivo
t").delete()
}
}
Here’s what the new migration does:
app.migrations.add(CreateAcronymCategoryPivo
t())
@Siblings(
through: AcronymCategoryPivot.self,
from: \.$acronym,
to: \.$category)
var categories: [Category]
acronymsRoutes.post(
":acronymID",
"categories",
":categoryID",
use: addCategoriesHandler)
This routes an HTTP POST request to
/api/acronyms/<ACRONYM_ID>/categories/<CA
TEGORY_ID> to addCategoriesHandler(_:).
URL:
http://localhost:8080/api/acronyms/<ACRO
NYM_ID>/categories/<CATEGORY_ID>
method: POST
Acronym’s categories
Open AcronymsController.swift and add a new
route handler after addCategoriesHandler(:_):
// 1
func getCategoriesHandler(_ req: Request)
-> EventLoopFuture<[Category]> {
// 2
Acronym.find(req.parameters.get("acronymI
D"), on: req.db)
.unwrap(or: Abort(.notFound))
.flatMap { acronym in
// 3
acronym.$categories.query(on: req.db).
all()
}
}
acronymsRoutes.get(
":acronymID",
"categories",
use: getCategoriesHandler)
URL:
http://localhost:8080/api/acronyms/<ACRO
NYM_ID>/categories
method: GET
@Siblings(
through: AcronymCategoryPivot.self,
from: \.$category,
to: \.$acronym)
var acronyms: [Acronym]
// 1
func getAcronymsHandler(_ req: Request)
-> EventLoopFuture<[Acronym]> {
// 2
Category.find(req.parameters.get("categoryI
D"), on: req.db)
.unwrap(or: Abort(.notFound))
.flatMap { category in
// 3
category.$acronyms.get(on: req.db)
}
}
categoriesRoute.get(
":categoryID",
"acronyms",
use: getAcronymsHandler)
URL:
http://localhost:8080/api/categories/<CATE
GORY_ID>/acronyms
method: GET
Send the request and you’ll receive an array of
the acronyms in that category:
acronymsRoutes.delete(
":acronymID",
"categories",
":categoryID",
use: removeCategoriesHandler)
URL:
http://localhost:8080/api/acronyms/<ACRO
NYM_ID>/categories/<CATEGORY_ID>
method: DELETE
Testing users
Writing your first test
Create a new file in Tests/AppTests called
UserTests.swift. This file will contain all the
user-related tests. Open the new file and insert
the following:
// 2
let app = Application(.testing)
// 3
defer { app.shutdown() }
// 4
try configure(app)
// 5
let user = User(
name: expectedName,
username: expectedUsername)
try user.save(on: app.db).wait()
try User(name: "Luke", username: "lukes")
.save(on: app.db)
.wait()
// 6
try app.test(.GET, "/api/users", afterResp
onse: { response in
// 7
XCTAssertEqual(response.status, .ok)
// 8
let users = try response.content.decode
([User].self)
// 9
XCTAssertEqual(users.count, 2)
XCTAssertEqual(users[0].name, expectedNa
me)
XCTAssertEqual(users[0].username, expect
edUsername)
XCTAssertEqual(users[0].id, user.id)
})
}
try app.autoRevert().wait()
try app.autoMigrate().wait()
This adds commands to revert any migrations in
the database and then run the migrations again.
This provides you with a clean database for
every test.
Test extensions
The first test contains a lot of code that all tests
need. Extract the common parts to make the
tests easier to read and to simplify future tests.
In Tests/AppTests create a new file for one of
these extensions, called
Application+Testable.swift. Open the new file
and add the following:
import XCTVapor
import App
extension Application {
static func testable() throws -> Applicati
on {
let app = Application(.testing)
try configure(app)
try app.autoRevert().wait()
try app.autoMigrate().wait()
return app
}
}
extension User {
static func create(
name: String = "Luke",
username: String = "lukes",
on database: Database
) throws -> User {
let user = User(name: name, username: us
ername)
try user.save(on: database).wait()
return user
}
}
XCTAssertEqual(users.count, 2)
XCTAssertEqual(users[0].name, usersName)
XCTAssertEqual(users[0].username, usersU
sername)
XCTAssertEqual(users[0].id, user.id)
})
}
// 2
try app.test(.POST, usersURI, beforeReques
t: { req in
// 3
try req.content.encode(user)
}, afterResponse: { response in
// 4
let receivedUser = try response.content.
decode(User.self)
// 5
XCTAssertEqual(receivedUser.name, usersN
ame)
XCTAssertEqual(receivedUser.username, us
ersUsername)
XCTAssertNotNil(receivedUser.id)
// 6
try app.test(.GET, usersURI,
afterResponse: { secondResponse in
// 7
let users =
try secondResponse.content.decode
([User].self)
XCTAssertEqual(users.count, 1)
XCTAssertEqual(users[0].name, usersN
ame)
XCTAssertEqual(users[0].username, us
ersUsername)
XCTAssertEqual(users[0].id, received
User.id)
})
})
}
2. Use test(_:_:beforeRequest:afterResponse:)
to send a POST request to the API
// 2
try app.test(.GET, "\(usersURI)\(user.i
d!)",
afterResponse: { response in
let receivedUser = try response.conten
t.decode(User.self)
// 3
XCTAssertEqual(receivedUser.name, user
sName)
XCTAssertEqual(receivedUser.username,
usersUsername)
XCTAssertEqual(receivedUser.id, user.i
d)
})
}
if acronymsUser == nil {
acronymsUser = try User.create(on: dat
abase)
}
// 3
let acronym1 = try Acronym.create(
short: acronymShort,
long: acronymLong,
user: user,
on: app.db)
_ = try Acronym.create(
short: "LOL",
long: "Laugh Out Loud",
user: user,
on: app.db)
// 4
try app.test(.GET, "\(usersURI)\(user.i
d!)/acronyms",
afterResponse: { response in
let acronyms = try response.content.de
code([Acronym].self)
// 5
XCTAssertEqual(acronyms.count, 2)
XCTAssertEqual(acronyms[0].id, acronym
1.id)
XCTAssertEqual(acronyms[0].short, acro
nymShort)
XCTAssertEqual(acronyms[0].long, acron
ymLong)
})
}
extension App.Category {
static func create(
name: String = "Random",
on database: Database
) throws -> App.Category {
let category = Category(name: name)
try category.save(on: database).wait()
return category
}
}
Run all the tests to make sure they all work. You
should have a sea of green tests with every route
tested!
Testing on Linux
Earlier in the chapter you learned why testing
your application is important. For server-side
Swift, testing on Linux is especially important.
When you deploy your application to Heroku,
for instance, you’re deploying to an operating
system different from the one you used for
development. It’s vital that you test your
application on the same environment that you
deploy it on.
# 1
FROM swift:5.2
# 2
WORKDIR /package
# 3
COPY . ./
# 4
CMD ["swift", "test", "--enable-test-discove
ry"]
if (app.environment == .testing) {
databaseName = "vapor-test"
databasePort = 5433
} else {
if (app.environment == .testing) {
databaseName = "vapor-test"
if let testPort = Environment.get("DATABAS
E_PORT") {
databasePort = Int(testPort) ?? 5433
} else {
databasePort = 5433
}
} else {
cd TILApp
swift run
User
Category
// 3
init(resourcePath: String) {
guard let resourceURL = URL(string: base
URL) else {
fatalError("Failed to convert baseURL
to a URL")
}
self.resourceURL =
resourceURL.appendingPathComponent(res
ourcePath)
}
}
// 1
var acronyms: [Acronym] = []
// 2
let acronymsRequest =
ResourceRequest<Acronym>(resourcePath: "ac
ronyms")
switch acronymResult {
// 3
case .failure:
ErrorPresenter.showError(
message: "There was an error getting t
he acronyms",
on: self)
// 4
case .success(let acronyms):
DispatchQueue.main.async { [weak self] i
n
guard let self = self else { return }
self.acronyms = acronyms
self.tableView.reloadData()
}
}
}
Displaying acronyms
Still in AcronymsTableViewController.swift,
update tableView(_:numberOfRowsInSection:) to
return the correct number of acronyms by
replacing return 1 with the following:
return acronyms.count
let usersRequest =
ResourceRequest<User>(resourcePath: "user
s")
let categoriesRequest =
ResourceRequest<Category>(resourcePath: "c
ategories")
do {
// 8
let resource = try JSONDecoder()
.decode(ResourceType.self, from:
jsonData)
completion(.success(resource))
} catch {
// 9
completion(.failure(.decodingErro
r))
}
}
// 10
dataTask.resume()
// 11
} catch {
completion(.failure(.encodingError))
}
}
Next, open
CreateUserTableViewController.swift and
replace the implementation of save(_:) with the
following:
// 1
guard
let name = nameTextField.text,
!name.isEmpty
else {
ErrorPresenter
.showError(message: "You must specify
a name", on: self)
return
}
// 2
guard
let username = usernameTextField.text,
!username.isEmpty
else {
ErrorPresenter.showError(
message: "You must specify a usernam
e",
on: self)
return
}
// 3
let user = User(name: name, username: userna
me)
// 4
ResourceRequest<User>(resourcePath: "users")
.save(user) { [weak self] result in
switch result {
// 5
case .failure:
let message = "There was a problem sav
ing the user"
ErrorPresenter.showError(message: mess
age, on: self)
// 6
case .success:
DispatchQueue.main.async { [weak self]
in
self?.navigationController?
.popViewController(animated: true)
}
}
}
Selecting users
When you create an acronym with the API, you
must provide a user ID. Asking a user to
remember and input a UUID isn’t a good user
experience! The iOS app should allow a user to
select a user by name.
Open CreateAcronymTableViewController.swift
and create a new method under viewDidLoad() to
populate the User cell in the create acronym
form with a default user:
func populateUsers() {
// 1
let usersRequest =
ResourceRequest<User>(resourcePath: "use
rs")
populateUsers()
Open SelectUserTableViewController.swift.
Under:
self.selectedUser = selectedUser
if user.name == selectedUser.name {
cell.accessoryType = .checkmark
} else {
cell.accessoryType = .none
}
// 1
if segue.identifier == "UnwindSelectUserSegu
e" {
// 2
guard
let cell = sender as? UITableViewCell,
let indexPath = tableView.indexPath(for:
cell)
else {
return
}
// 3
selectedUser = users[indexPath.row]
}
// 1
guard let controller = segue.source
as? SelectUserTableViewController
else {
return
}
// 2
selectedUser = controller.selectedUser
userLabel.text = selectedUser?.name
// 2
let acronym = Acronym(
short: shortText,
long: longText,
userID: userID)
let acronymSaveData = acronym.toCreateData()
// 3
ResourceRequest<Acronym>(resourcePath: "acro
nyms")
.save(acronymSaveData) { [weak self] resul
t in
switch result {
// 4
case .failure:
let message = "There was a problem sav
ing the acronym"
ErrorPresenter.showError(message: mess
age, on: self)
// 5
case .success:
DispatchQueue.main.async { [weak self]
in
self?.navigationController?
.popViewController(animated: true)
}
}
}
Open AcronymsTableViewController.swift.
Replace the implementation for
makeAcronymsDetailTableViewController(_:) with
the following:
// 1
guard let indexPath = tableView.indexPathFor
SelectedRow else {
return nil
}
// 2
let acronym = acronyms[indexPath.row]
// 3
return AcronymDetailTableViewController(
coder: coder,
acronym: acronym)
3. Create an AcronymDetailTableViewController
using the selected acronym.
struct AcronymRequest {
let resource: URL
init(acronymID: UUID) {
let resourceString =
"http://localhost:8080/api/acronyms/\
(acronymID)"
guard let resourceURL = URL(string: reso
urceString) else {
fatalError("Unable to createURL")
}
self.resource = resourceURL
}
}
This sets the resource property to the URL for
that acronym. At the bottom of AcronymRequest,
add a method to get the acronym’s user:
func getUser(
completion: @escaping (
Result<User, ResourceRequestError>
) -> Void
) {
// 1
let url = resource.appendingPathComponent
("user")
// 2
let dataTask = URLSession.shared
.dataTask(with: url) { data, _, _ in
// 3
guard let jsonData = data else {
completion(.failure(.noData))
return
}
do {
// 4
let user = try JSONDecoder()
.decode(User.self, from: jsonData)
completion(.success(user))
} catch {
// 5
completion(.failure(.decodingError))
}
}
// 6
dataTask.resume()
}
Open AcronymDetailTableViewController.swift
and add the following implementation to
getAcronymData():
// 1
guard let id = acronym.id else {
return
}
// 2
let acronymDetailRequester = AcronymRequest
(acronymID: id)
// 3
acronymDetailRequester.getUser { [weak self]
result in
switch result {
case .success(let user):
self?.user = user
case .failure:
let message =
"There was an error getting the acrony
m’s user"
ErrorPresenter.showError(message: messag
e, on: self)
}
}
// 4
acronymDetailRequester.getCategories { [weak
self] result in
switch result {
case .success(let categories):
self?.categories = categories
case .failure:
let message =
"There was an error getting the acrony
m’s categories"
ErrorPresenter.showError(message: messag
e, on: self)
}
}
the acronym
its meaning
its user
its categories
Return to
CreateAcronymTableViewController.swift.
Inside save(_:) after:
Next, open
AcronymsDetailTableViewController.swift and
add the following implementation to the end of
prepare(for:sender:):
if segue.identifier == "EditAcronymSegue" {
// 1.
guard
let destination = segue.destination
as? CreateAcronymTableViewController e
lse {
return
}
// 2.
destination.selectedUser = user
destination.acronym = acronym
}
user = controller.selectedUser
if let acronym = controller.acronym {
self.acronym = acronym
}
Open AcronymsTableViewController.swift. To
enable deletion of a table row, add the following
after tableView(_:cellForRowAt:):
override func tableView(
_ tableView: UITableView,
commit editingStyle: UITableViewCell.Editi
ngStyle,
forRowAt indexPath: IndexPath
) {
if let id = acronyms[indexPath.row].id {
// 1
let acronymDetailRequester = AcronymRequ
est(acronymID: id)
acronymDetailRequester.delete()
}
// 2
acronyms.remove(at: indexPath.row)
// 3
tableView.deleteRows(at: [indexPath], wit
h: .automatic)
}
// 2
let category = Category(name: name)
// 3
ResourceRequest<Category>(resourcePath: "cat
egories")
.save(category) { [weak self] result in
switch result {
// 5
case .failure:
let message = "There was a problem sav
ing the category"
ErrorPresenter.showError(message: mess
age, on: self)
// 6
case .success:
DispatchQueue.main.async { [weak self]
in
self?.navigationController?
.popViewController(animated: true)
}
}
}
Open
AcronymsDetailTableViewController.swift.
Change the return statement in
numberOfSections(in:) to:
return 5
// 1
case 4:
cell.textLabel?.text = "Add To Category"
These steps:
Open AddToCategoryTableViewController.swift
and add the following implementation to
loadData() to get all the categories from the API:
// 1
let categoriesRequest =
ResourceRequest<Category>(resourcePath: "c
ategories")
// 2
categoriesRequest.getAll { [weak self] resul
t in
switch result {
// 3
case .failure:
let message =
"There was an error getting the catego
ries"
ErrorPresenter.showError(message: messag
e, on: self)
// 4
case .success(let categories):
self?.categories = categories
DispatchQueue.main.async { [weak self] i
n
self?.tableView.reloadData()
}
}
}
Open AddToCategoryTableViewController.swift
and add the following extension at the end of the
file:
// MARK: - UITableViewDelegate
extension AddToCategoryTableViewController {
override func tableView(
_ tableView: UITableView,
didSelectRowAt indexPath: IndexPath
) {
// 1
let category = categories[indexPath.row]
// 2
guard let acronymID = acronym.id else {
let message = """
There was an error adding the acrony
m
to the category - the acronym has no
ID
"""
ErrorPresenter.showError(message: mess
age, on: self)
return
}
// 3
let acronymRequest = AcronymRequest(acro
nymID: acronymID)
acronymRequest
.add(category: category) { [weak self]
result in
switch result {
// 4
case .success:
DispatchQueue.main.async { [weak s
elf] in
self?.navigationController?
.popViewController(animated: t
rue)
}
// 5
case .failure:
let message = """
There was an error adding the ac
ronym
to the category
"""
ErrorPresenter.showError(message:
message, on: self)
}
}
}
}
Finally, open
AcronymDetailTableViewController.swift to set
up AddToCategoryTableViewController. Change the
implementation of
makeAddToCategoryController(_:) to the
following:
AddToCategoryTableViewController(
coder: coder,
acronym: acronym,
selectedCategories: categories)
Leaf
Leaf is Vapor’s templating language. A
templating language allows you to pass
information to a page so it can generate the
final HTML without knowing everything up
front. For example, in the TIL application, you
don’t know every acronym that users will create
when you deploy your application. Templating
allows you handle this with ease.
Templating languages also allow you to reduce
duplication in your webpages. Instead of
multiple pages for acronyms, you create a single
template and set the properties specific to
displaying a particular acronym. If you decide to
change the way you display an acronym, you
only need change your code in one place and all
acronym pages will show the new format.
Configuring Leaf
To use Leaf, you need to add it to your project as
a dependency. Using the TIL application from
Chapter 11, “Testing”, or the starter project from
this chapter, open Package.swift. Replace its
contents with the following:
// swift-tools-version:5.2
import PackageDescription
mkdir -p Resources/Views
Rendering a page
Open WebsiteController.swift and replace its
contents with the following, to create a new
type to hold all the website routes and a route
that returns an index template:
import Vapor
import Leaf
// 1
struct WebsiteController: RouteCollection {
// 2
func boot(routes: RoutesBuilder) throws {
// 3
routes.get(use: indexHandler)
}
// 4
func indexHandler(_ req: Request)
-> EventLoopFuture<View> {
// 5
return req.view.render("index")
}
}
app.get { req in
return "It works!"
}
import Leaf
app.views.use(.leaf)
This tells Vapor to use Leaf when rendering
views and LeafRenderer when asked for a
ViewRenderer type.
Injecting variables
The template is currently just a static page and
not at all impressive! To make the page more
dynamic, open index.leaf and change the <title>
line to the following:
<title>#(title) | Acronyms</title>
Using tags
The home page of the TIL website should
display a list of all the acronyms. Still in
WebsiteController.swift, add a new property to
IndexContext underneath title:
<!-- 2 -->
#if(acronyms):
<!-- 3 -->
<table>
<thead>
<tr>
<th>Short</th>
<th>Long</th>
</tr>
</thead>
<tbody>
<!-- 4 -->
#for(acronym in acronyms):
<tr>
<!-- 5 -->
<td>#(acronym.short)</td>
<td>#(acronym.long)</td>
</tr>
#endfor
</tbody>
</table>
<!-- 6 -->
#else:
<h2>There aren’t any acronyms yet!</h2>
#endif
<!-- 5 -->
<p>Created by #(user.name)</p>
</body>
</html>
<td><a href="/acronyms/#(acronym.id)">#(acro
nym.short)</a></td>
Embedding templates
Currently, if you change the index page
template to add styling, you’ll affect only that
page. You’d have to duplicate the styling in the
acronym detail page, and any other future
pages.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>#(title) | Acronyms</title>
</head>
<body>
</body>
</html>
#import("content")
#extend("base"):
#endextend
#if(acronyms):
<table>
<thead>
<tr>
<th>Short</th>
<th>Long</th>
</tr>
</thead>
<tbody>
#for(acronym in acronyms):
<tr>
<td>
<a href="/acronyms/#(acronym.i
d)">
#(acronym.short)
</a>
</td>
<td>#(acronym.long)</td>
</tr>
#endfor
</tbody>
</table>
#else:
<h2>There aren’t any acronyms yet!</h2>
#endif
#endexport
#extend("base"):
#export("content"):
<h1>#(acronym.short)</h1>
<h2>#(acronym.long)</h2>
<p>Created by #(user.name)</p>
#endexport
#endextend
Bootstrap
Bootstrap is an open-source, front-end
framework for websites, originally built by
Twitter. It provides easy-to-use components
that you add to web pages. It’s a mobile-first
library and makes it simple to build a site that
works on screens of all sizes.
Tables
Bootstrap provides classes to style tables with
ease. Open index.leaf and replace the <table>
tag with the following:
<table class="table table-bordered table-hov
er">
<thead class="thead-light">
<img src="/images/logo.png"
class="mx-auto d-block" alt="TIL Logo" />
<!-- 5 -->
#if(count(acronyms) > 0):
<table class="table table-bordered tab
le-hover">
<thead class="thead-light">
<tr>
<th>Short</th>
<th>Long</th>
</tr>
</thead>
<tbody>
<!-- 6 -->
#for(acronym in acronyms):
<tr>
<td>
<a href="/acronyms/#(acrony
m.id)">
#(acronym.short)
</a>
</td>
<td>#(acronym.long)</td>
</tr>
#endfor
</tbody>
</table>
#else:
<h2>There aren’t any acronyms yet!</h2
>
#endif
#endexport
#endextend
<!-- 3 -->
#if(count(users) > 0):
<table class="table table-bordered tab
le-hover">
<thead class="thead-light">
<tr>
<th>Username</th>
<th>Name</th>
</tr>
</thead>
<tbody>
#for(user in users):
<tr>
<td>
<a href="/users/#(user.id)">
#(user.username)
</a>
</td>
<td>#(user.name)</td>
</tr>
#endfor
</tbody>
</table>
#else:
<h2>There aren’t any users yet!</h2>
#endif
#endexport
#endextend
// 1
func allUsersHandler(_ req: Request)
-> EventLoopFuture<View> {
// 2
User.query(on: req.db)
.all()
.flatMap { users in
// 3
let context = AllUsersContext(
title: "All Users",
users: users)
return req.view.render("allUsers", c
ontext)
}
}
#extend("acronymsTable")
#extend("acronymsTable")
Categories
You’ve created pages for viewing acronyms and
users. Now it’s time to create similar pages for
categories. Open WebsiteController.swift. At the
bottom of the file, add a context for the “All
Categories” page:
<!-- 2 -->
#if(count(categories) > 0):
<table class="table table-bordered tab
le-hover">
<thead class="thead-light">
<tr>
<th>Name</th>
</tr>
</thead>
<tbody>
<!-- 3 -->
#for(category in categories):
<tr>
<td>
<a href="/categories/#(categ
ory.id)">
#(category.name)
</a>
</td>
</tr>
#endfor
</tbody>
</table>
#else:
<h2>There aren’t any categories yet!</
h2>
#endif
#endexport
#endextend
#extend("base"):
#export("content"):
<h1>#(category.name)</h1>
#extend("acronymsTable")
#endexport
#endextend
// 1
routes.get("categories", use: allCategoriesH
andler)
// 2
routes.get("categories", ":categoryID", use:
categoryHandler)
// 1
func createAcronymPostHandler(_ req: Reques
t) throws
-> EventLoopFuture<Response> {
// 2
let data = try req.content.decode(CreateAc
ronymData.self)
let acronym = Acronym(
short: data.short,
long: data.long,
userID: data.userID)
// 3
return acronym.save(on: req.db).flatMapThro
wing {
// 4
guard let id = acronym.id else {
throw Abort(.internalServerError)
}
// 5
return req.redirect(to: "/acronyms/\(i
d)")
}
}
Here’s what this does:
<!-- 2 -->
<form method="post">
<!-- 3 -->
<div class="form-group">
<label for="short">Acronym</label>
<input type="text" name="short" clas
s="form-control"
id="short"/>
</div>
<!-- 4 -->
<div class="form-group">
<label for="long">Meaning</label>
<input type="text" name="long" class
="form-control"
id="long"/>
</div>
<div class="form-group">
<label for="userID">User</label>
<!-- 5 -->
<select name="userID" class="form-co
ntrol" id="userID">
<!-- 6 -->
#for(user in users):
<option value="#(user.id)">
#(user.name)
</option>
#endfor
</select>
</div>
<!-- 7 -->
<button type="submit" class="btn btn-p
rimary">
Submit
</button>
</form>
#endexport
#endextend
routes.get(
"acronyms", ":acronymID", "edit",
use: editAcronymHandler)
routes.post(
"acronyms", ":acronymID", "edit",
use: editAcronymPostHandler)
This registers a route at /acronyms/<ACRONYM
ID>/edit to accept GET requests that calls
editAcronymHandler(_:). It also registers a route
to handle POST requests to the same URL that
calls editAcronymPostHandler(_:).
<option value="#(user.id)"
#if(editing): #if(acronym.user.id == user.i
d):
selected #endif #endif>
#(user.name)
</option>
Deleting acronyms
Unlike creating and editing acronyms, deleting
an acronym only requires a single route.
However, with web browsers there’s no simple
way to send a DELETE request.
Browsers can only send GET requests to request
a page and POST requests to send data with
forms.
routes.post(
"acronyms", ":acronymID", "delete",
use: deleteAcronymHandler)
<!-- 1 -->
<form method="post" action="/acronyms/#(acro
nym.id)/delete">
<!-- 2 -->
<a class="btn btn-primary" href="/acronym
s/#(acronym.id)/edit"
role="button">Edit</a>
<!-- 3 -->
<input class="btn btn-danger" type="submi
t" value="Delete" />
</form>
<!-- 1 -->
<div class="form-group">
<!-- 2 -->
<label for="categories">Categories</label>
<!-- 3 -->
<select name="categories[]" class="form-co
ntrol"
id="categories" placeholder="Categories"
multiple="multiple">
</select>
</div>
<!-- 1 -->
<script src="https://code.jquery.com/jquery-
3.5.1.min.js" integrity="sha384-ZvpUoO/+PpLX
R1lu4jmpXWu80pZlYUAfxl5NsBMWOEPSjUn/6Z/hRTt8
+pR6L4N2" crossorigin="anonymous"></script>
<!-- 2 -->
#if(title == "Create An Acronym" || title ==
"Edit Acronym"):
<script src="https://cdnjs.cloudflare.com/a
jax/libs/select2/4.0.13/js/select2.min.js" i
ntegrity="sha384-JnbsSLBmv2/R0fUmF2XYIcAEMPH
EAO51Gitn9IjL4l89uFTIgtLF1+jqIqqd9FSk" cross
origin="anonymous"></script>
<!-- 3 -->
<script src="/scripts/createAcronym.js"></
script>
#endif
{
"id": <id of the category>,
"text": <name of the category>
}
<!-- 1 -->
#if(count(categories) > 0):
<!-- 2 -->
<h3>Categories</h3>
<ul>
<!-- 3 -->
#for(category in categories):
<li>
<a href="/categories/#(category.i
d)">
#(category.name)
</a>
</li>
#endfor
</ul>
#endif
In acronymHandler(_:), replace:
In editAcronymHandler(_:) replace:
let context = EditAcronymContext(acronym: ac
ronym, users: users)
return req.view.render("createAcronym", cont
ext)
acronym.$categories.get(on: req.db).flatMap {
categories in
let context = EditAcronymContext(
acronym: acronym,
users: users,
categories: categories)
return req.view.render("createAcronym", co
ntext)
}
// 5
let existingSet = Set<String>(existi
ngStringArray)
let newSet = Set<String>(updateData.
categories ?? [])
// 6
let categoriesToAdd = newSet.subtrac
ting(existingSet)
let categoriesToRemove = existingSet
.subtracting(newSet)
// 7
var categoryResults: [EventLoopFutur
e<Void>] = []
// 8
for newCategory in categoriesToAdd {
categoryResults.append(
Category.addCategory(
newCategory,
to: acronym,
on: req))
}
// 9
for categoryNameToRemove in categori
esToRemove {
// 10
let categoryToRemove = existingCat
egories.first {
$0.name == categoryNameToRemove
}
// 11
if let category = categoryToRemove
{
categoryResults.append(
acronym.$categories.detach(cat
egory, on: req.db))
}
}
The TIL app contains both the API and the web
app. This works well for small applications, but
for very large applications you may consider
splitting them up into their own apps. The web
app then talks to the API like any other client
would, such as the iOS app. This allows you to
scale the different parts separately. Large
applications may even be developed by different
teams. Splitting them up lets the application
grow and change, without reliance on the other
team.
In the next section of the book, you’ll learn how
to apply authentication to your application.
Currently anyone can create any acronyms in
both the iOS app and the web app. This isn’t
desirable, especially for large systems. The next
chapters show you how to protect both the API
and web app with authentication.
Section III: Validation,
Users & Authentication
This section shows you how to protect your
Vapor application with authentication. You’ll
learn how to add password protection to both
the API and the website, which lets you require
users to log in. You’ll learn about different types
of authentication: HTTP Basic authentication
and token-based authentication for the API, and
cookie- and session-based authentication for
the web site.
@Field(key: "password")
var password: String
Password storage
Thanks to Codable, you don’t have to make any
additional changes to create users with
passwords. The existing UserController now
automatically expects to find the password
property in the incoming JSON. However,
without any changes, you’ll be saving the user’s
password in plain text.
URL: http://localhost:8080/api/users/
method: POST
extension User {
// 1
func convertToPublic() -> User.Public {
// 2
return User.Public(id: id, name: name, u
sername: username)
}
}
// 5
extension Collection where Element: User {
// 6
func convertToPublic() -> [User.Public] {
// 7
return self.map { $0.convertToPublic() }
}
}
// 8
extension EventLoopFuture where Value == Arr
ay<User> {
// 9
func convertToPublic() -> EventLoopFuture<
[User.Public]> {
// 10
return self.map { $0.convertToPublic() }
}
}
User.query(on: req.db).all().convertToPublic
()
User.find(req.parameters.get("userID"), on: r
eq.db)
.unwrap(or: Abort(.notFound))
.convertToPublic()
Finally, open AcronymsController.swift and
replace getUserHandler(_:) so it returns a public
user:
// 1
func getUserHandler(_ req: Request)
-> EventLoopFuture<User.Public> {
Acronym.find(req.parameters.get("acronymI
D"), on: req.db)
.unwrap(or: Abort(.notFound))
.flatMap { acronym in
// 2
acronym.$user.get(on: req.db).convertToP
ublic()
}
}
timc:password
dGltYzpwYXNzd29yZA==
// 1
extension User: ModelAuthenticatable {
// 2
static let usernameKey = \User.$username
// 3
static let passwordHashKey = \User.$passwo
rd
// 4
func verify(password: String) throws -> Bo
ol {
try Bcrypt.verify(password, created: sel
f.password)
}
}
// 1
let basicAuthMiddleware = User.authenticator
()
// 2
let guardAuthMiddleware = User.guardMiddlewa
re()
// 3
let protected = acronymsRoutes.grouped(
basicAuthMiddleware,
guardAuthMiddleware)
// 4
protected.post(use: createHandler)
2. Create an instance of
GuardAuthenticationMiddleware which
ensures that requests contain authenticated
users.
acronymsRoutes.post(use: createHandler)
method: POST
short: OMG
long: Oh My God
import Vapor
import Fluent
@ID
var id: UUID?
@Field(key: "value")
var value: String
@Parent(key: "userID")
var user: User
init() {}
app.migrations.add(CreateToken())
extension Token {
// 1
static func generate(for user: User) throw
s -> Token {
// 2
let random = [UInt8].random(count: 16).b
ase64
// 3
return try Token(value: random, userID:
user.requireID())
}
}
// 1
func loginHandler(_ req: Request) throws
-> EventLoopFuture<Token> {
// 2
let user = try req.auth.require(User.self)
// 3
let token = try Token.generate(for: user)
// 4
return token.save(on: req.db).map { token
}
}
// 1
let basicAuthMiddleware = User.authenticator
()
let basicAuthGroup = usersRoute.grouped(basi
cAuthMiddleware)
// 2
basicAuthGroup.post("login", use: loginHandl
er)
2. Connect /api/users/login to
loginHandler(_:) through the protected
group.
let createAcronymData =
CreateAcronymData(short: acronymShort, lon
g: acronymLong)
Return to AcronymsController.swift. In
boot(routes:), remove the code you used earlier
to protect the “create an acronym” route and
replace it with the following:
// 1
let tokenAuthMiddleware = Token.authenticato
r()
let guardAuthMiddleware = User.guardMiddlewa
re()
// 2
let tokenAuthGroup = acronymsRoutes.grouped(
tokenAuthMiddleware,
guardAuthMiddleware)
// 3
tokenAuthGroup.post(use: createHandler)
1. Create a ModelTokenAuthenticator
middleware for Token. This extracts the
bearer token out of the request and
converts it into a logged in user.
2. Create a route group using
tokenAuthMiddleware and guardAuthMiddleware
to protect the route for creating an acronym
with token authentication.
URL: http://localhost:8080/api/acronyms/
method: POST
short: IKR
// 1
struct CreateAdminUser: Migration {
// 2
func prepare(on database: Database) -> Eve
ntLoopFuture<Void> {
// 3
let passwordHash: String
do {
passwordHash = try Bcrypt.hash("passwo
rd")
} catch {
return database.eventLoop.future(erro
r: error)
}
// 4
let user = User(
name: "Admin",
username: "admin",
password: passwordHash)
// 5
return user.save(on: database)
}
// 6
func revert(on database: Database) -> Even
tLoopFuture<Void> {
// 7
User.query(on: database)
.filter(\.$username == "admin")
.delete()
}
}
app.migrations.add(CreateAdminUser())
import Vapor
// 4
let password = try Bcrypt.hash("password")
let user = User(
name: name,
username: createUsername,
password: password)
try user.save(on: database).wait()
return user
}
# 1
docker rm -f postgres-test
# 2
docker run --name postgres-test -e POSTGRES_
DB=vapor-test \
-e POSTGRES_USER=vapor_username \
-e POSTGRES_PASSWORD=vapor_password \
-p 5433:5432 -d postgres
import XCTVapor
import App
// 3
if (loggedInRequest || loggedInUser != ni
l) {
let userToLogin: User
// 4
if let user = loggedInUser {
userToLogin = user
} else {
userToLogin = User(
name: "Admin",
username: "admin",
password: "password")
}
// 5
let token = try login(user: userToLogin)
// 6
request.headers.bearerAuthorization =
.init(token: token.value)
}
// 7
try beforeRequest(&request)
// 8
do {
let response = try performTest(request:
request)
try afterResponse(response)
} catch {
XCTFail("\(error)", file: (file), line: li
ne)
throw error
}
return self
}
try app.test(
.DELETE,
"\(acronymsURI)\(acronym.id!)",
loggedInRequest: true)
try app.test(
.DELETE,
"\(acronymsURI)\(acronym.id!)/categories/\
(category.id!)",
loggedInRequest: true)
Next, in
testGettingACategoriesAcronymsFromTheAPI(),
replace the two POST requests with the
following to use an authenticated user:
try app.test(
.POST,
"/api/acronyms/\(acronym.id!)/categories/\
(category.id!)",
loggedInRequest: true)
try app.test(
.POST,
"/api/acronyms/\(acronym2.id!)/categories/
\(category.id!)",
loggedInRequest: true)
to the following:
// 1
try app.test(.POST, usersURI, loggedInReques
t: true,
beforeRequest: { req in
try req.content.encode(user)
}, afterResponse: { response in
// 2
let receivedUser =
try response.content.decode(User.Public.
self)
XCTAssertEqual(receivedUser.name, usersNam
e)
XCTAssertEqual(receivedUser.username, user
sUsername)
XCTAssertNotNil(receivedUser.id)
Logging in
Open AppDelegate.swift. In
application(_:didFinishLaunchingWithOptions:),
the application checks the new Auth object for a
token. If there’s no token, it launches the login
screen; otherwise, it displays the acronyms table
as normal.
// 4
var loginRequest = URLRequest(url: url)
// 5
loginRequest.addValue(
"Basic \(loginString)",
forHTTPHeaderField: "Authorization")
loginRequest.httpMethod = "POST"
// 6
let dataTask = URLSession.shared
.dataTask(with: loginRequest) { data, re
sponse, _ in
// 7
guard
let httpResponse = response as? HTTP
URLResponse,
httpResponse.statusCode == 200,
let jsonData = data
else {
completion(.failure)
return
}
do {
// 8
let token = try JSONDecoder()
.decode(Token.self, from: jsonDat
a)
// 9
self.token = token.value
completion(.success)
} catch {
// 10
completion(.failure)
}
}
// 11
dataTask.resume()
}
// 1
token = nil
DispatchQueue.main.async {
guard let applicationDelegate =
UIApplication.shared.delegate as? AppDel
egate else {
return
}
// 2
let rootController =
UIStoryboard(name: "Login", bundle: Bund
le.main)
.instantiateViewController(
withIdentifier: "LoginNavigation")
applicationDelegate.window?.rootViewContro
ller =
rootController
}
Creating models
The starter project simplifies
CreateAcronymTableViewController as you no
longer have to provide a user when creating an
acronym. Open ResourceRequest.swift. In
save(_:completion:) before var urlRequest =
URLRequest(url: resourceURL) add the following:
// 1
guard let token = Auth().token else {
// 2
Auth().logout()
return
}
Next, under
urlRequest.addValue("application/json",
forHTTPHeaderField: "Content-Type") add:
urlRequest.addValue(
"Bearer \(token)",
forHTTPHeaderField: "Authorization")
urlRequest.addValue(
"Bearer \(token)",
forHTTPHeaderField: "Authorization")
urlRequest.addValue(
"Bearer \(token)",
forHTTPHeaderField: "Authorization")
Web authentication
How it works
Earlier, you learned how to use HTTP basic
authentication and bearer authentication to
protect the API. As you’ll recall, this works by
sending tokens and credentials in the request
headers. However, this isn’t possible in web
browsers. There’s no way to add headers to
requests your browser makes with normal
HTML.
app.middleware.use(app.sessions.middleware)
// 1
extension User: ModelSessionAuthenticatable
{}
// 2
extension User: ModelCredentialsAuthenticata
ble {}
2. Conform User to
ModelCredentialsAuthenticatable. This allows
Vapor to authenticate users with a
username and password when they log in.
Since you’ve already implemented the
necessary properties and function for
ModelCredentialsAuthenticatable in
ModelAuthenticatable, there’s nothing to do
here.
Log in
To log a user in, you need two routes — one for
showing the login page and one for accepting
the POST request from that page. Open
WebsiteController.swift and add the following at
the bottom of the file to create a context for the
login page:
struct LoginContext: Encodable {
let title = "Log In"
let loginError: Bool
// 1
func loginHandler(_ req: Request)
-> EventLoopFuture<View> {
let context: LoginContext
// 2
if let error = req.query[Bool.self, at:
"error"], error {
context = LoginContext(loginError: tru
e)
} else {
context = LoginContext()
}
// 3
return req.view.render("login", context)
}
Here’s what this does:
<!-- 3 -->
#if(loginError):
<div class="alert alert-danger" role
="alert">
User authentication error. Either yo
ur username or
password was invalid.
</div>
#endif
<!-- 4 -->
<form method="post">
<!-- 5 -->
<div class="form-group">
<label for="username">Username</labe
l>
<input type="text" name="username" c
lass="form-control"
id="username"/>
</div>
<!-- 6 -->
<div class="form-group">
<label for="password">Password</labe
l>
<input type="password" name="passwor
d"
class="form-control" id="password"/
>
</div>
<!-- 7 -->
<button type="submit" class="btn btn-p
rimary">
Log In
</button>
</form>
#endexport
#endextend
// 1
routes.get("login", use: loginHandler)
// 2
let credentialsAuthRoutes =
routes.grouped(User.credentialsAuthenticat
or())
// 3
credentialsAuthRoutes.post("login", use: log
inPostHandler)
let authSessionsRoutes =
routes.grouped(User.sessionAuthenticator
())
This creates a route group that runs
DatabaseSessionAuthenticator before the route
handlers. This middleware reads the cookie from
the request and looks up the session ID in the
application’s session list. If the session contains
a user, DatabaseSessionAuthenticator adds it to
the request’s authentication cache, making the
user available later in the process.
acronym.$user.id = userID
Log out
When you allow users to log in to your site, you
should also allow them to log out. Still in
WebsiteController.swift, add the following after
loginPostHandler(_:):
// 1
func logoutHandler(_ req: Request) -> Respon
se {
// 2
req.auth.logout(User.self)
// 3
return req.redirect(to: "/")
}
<!-- 1 -->
#if(userLoggedIn):
<!-- 2 -->
<form class="form-inline" action="/logout"
method="POST">
<!-- 3 -->
<input class="nav-link btn btn-secondary
mr-sm-2"
type="submit" value="Log out">
</form>
#endif
Cookies
Cookies are widely used on the web. Everyone’s
seen the cookie consent messages that pop up
on a site when you first visit. You’ve already
used cookies to implement authentication, but
sometimes you want to set and read cookies
manually.
A common way to handle the cookie consent
message is to add a cookie when a user has
accepted the notice (the irony!).
<!-- 1 -->
#if(showCookieMessage):
<!-- 2 -->
<footer id="cookie-footer">
<div id="cookieMessage" class="containe
r">
<span class="muted">
<!-- 3 -->
This site uses cookies! To accept th
is, click
<a href="#" onclick="cookiesConfirmed
()">OK</a>
</span>
</div>
</footer>
<!-- 4 -->
<script src="/scripts/cookies.js"></script
>
#endif
undefined
touch Public/scripts/cookies.js
Sessions
In addition to using cookies for web
authentication, you’ve also made use of
sessions. Sessions are useful in a number of
scenarios, including authentication.
// 1
let token = [UInt8].random(count: 16).base64
// 2
let context = CreateAcronymContext(csrfToke
n: token)
// 3
req.session.data["CSRF_TOKEN"] = token
#if(csrfToken):
<input type="hidden" name="csrfToken" valu
e="#(csrfToken)">
#endif
// 1
let expectedToken = req.session.data["CSRF_T
OKEN"]
// 2
req.session.data["CSRF_TOKEN"] = nil
// 3
guard
let csrfToken = data.csrfToken,
expectedToken == csrfToken
else {
throw Abort(.badRequest)
}
<form method="post">
<div class="form-group">
<label for="name">Name</label>
<input type="text" name="name" class
="form-control"
id="name"/>
</div>
<div class="form-group">
<label for="username">Username</labe
l>
<input type="text" name="username" c
lass="form-control"
id="username"/>
</div>
<div class="form-group">
<label for="password">Password</labe
l>
<input type="password" name="passwor
d"
class="form-control" id="password"/>
</div>
<div class="form-group">
<label for="confirmPassword">Confirm P
assword</label>
<input type="password" name="confirmP
assword"
class="form-control" id="confirmPassw
ord"/>
</div>
name
username
password
password confirmation
// 1
func registerPostHandler(
_ req: Request
) throws -> EventLoopFuture<Response> {
// 2
let data = try req.content.decode(Register
Data.self)
// 3
let password = try Bcrypt.hash(data.passwo
rd)
// 4
let user = User(
name: data.name,
username: data.username,
password: password)
// 5
return user.save(on: req.db).map {
// 6
req.auth.login(user)
// 7
return req.redirect(to: "/")
}
}
Here’s what’s going on in the route handler:
<!-- 1 -->
#if(!userLoggedIn):
<!-- 2 -->
<li class="nav-item #if(title == "Registe
r"): active #endif">
<!-- 3 -->
<a href="/register" class="nav-link">Reg
ister</a>
</li>
#endif
Here’s what the new Leaf code does:
Basic validation
Vapor provides a validation module to help you
check data and models. Open
WebsiteController.swift and add the following at
the bottom:
// 1
extension RegisterData: Validatable {
// 2
public static func validations(
_ validations: inout Validations
) {
// 3
validations.add("name", as: String.self,
is: .ascii)
// 4
validations.add(
"username",
as: String.self,
is: .alphanumeric && .count(3...))
// 5
validations.add(
"password",
as: String.self,
is: .count(8...))
}
}
Custom validation
Vapor allows you to write expressive and
complex validations, but sometimes you need
more than the built-in options offer. For
example, you may want to validate a US Zip
code. To demonstrate this, at the bottom of
WebsiteController.swift, add the following:
// 1
extension ValidatorResults {
// 2
struct ZipCode {
let isValidZipCode: Bool
}
}
// 3
extension ValidatorResults.ZipCode: Validato
rResult {
// 4
var isFailure: Bool {
!isValidZipCode
}
// 5
var successDescription: String? {
"is a valid zip code"
}
// 6
var failureDescription: String? {
"is not a valid zip code"
}
}
Here’s what the new code does:
// 3
public static var zipCode: Validator<T> {
// 4
Validator { input -> ValidatorResult in
// 5
guard
let range = input.range(
of: zipCodeRegex,
options: [.regularExpression]),
range.lowerBound == input.startIndex
&& range.upperBound == input.endIn
dex
else {
// 6
return ValidatorResults.ZipCode(isVa
lidZipCode: false)
}
// 7
return ValidatorResults.ZipCode(isVali
dZipCode: true)
}
}
}
Displaying an error
Currently, when a user fills out the form
incorrectly, the application redirects back to the
form with no indication of what went wrong.
Open register.leaf and add the following under
<h1>#(title)</h1>:
#if(message):
<div class="alert alert-danger" role="aler
t">
Please fix the following errors:<br />
#(message)
</div>
#endif
In registerHandler(_:), replace:
let context = RegisterContext()
OAuth 2.0
OAuth 2.0 is an authorization framework that
allows third-party applications to access
resources on behalf of a user. Whenever you log
in to a website with your Google account, you’re
using OAuth.
When you click Login with Google, Google is the
site that authenticates you. You then authorize
the application to have access to your Google
data, such as your email. Once you’ve allowed
the application access, Google gives the
application a token. The app uses this token to
authenticate requests to Google APIs. You’ll
implement this technique in this chapter.
Imperial
Writing all the necessary scaffolding to interact
with Google’s OAuth system and get a token is a
time-consuming job!
.package(
url: "https://github.com/vapor/leaf.git",
from: "4.0.0")
.package(
url: "https://github.com/vapor/leaf.git",
from: "4.0.0"),
.package(
url: "https://github.com/vapor-community/I
mperial.git",
from: "1.0.0")
import ImperialGoogle
import Vapor
import Fluent
// 3
let googleAPIURL: URI =
"https://www.googleapis.com/oauth2/v
1/userinfo?alt=json"
// 4
return request
.client
.get(googleAPIURL, headers: headers)
.flatMapThrowing { response in
// 5
guard response.status == .ok else {
// 6
if response.status == .unauthorize
d {
throw Abort.redirect(to: "/login
-google")
} else {
throw Abort(.internalServerErro
r)
}
}
// 7
return try response.content
.decode(GoogleUserInfo.self)
}
}
}
<a href="/login-google">
<img class="mt-3" src="/images/sign-in-wit
h-google.png"
alt="Sign In With Google">
</a>
Next, in processGoogleLogin(request:token:),
replace:
Finally, replace:
return request.eventLoop
.future(request.redirect(to: "/"))
// 1
guard let googleAuthURL = URL(
string: "http://localhost:8080/iOS/login-g
oogle")
else {
return
}
// 2
let scheme = "tilapp"
// 3
let session = ASWebAuthenticationSession(
url: googleAuthURL,
callbackURLScheme: scheme) { callbackURL,
error in
}
3. Create an instance of
ASWebAuthenticationSession. This allows the
user to authenticate with the TIL app using
existing credentials from Safari.
// 1
guard
error == nil,
let callbackURL = callbackURL
else {
return
}
// 2
let queryItems =
URLComponents(string: callbackURL.absolute
String)?.queryItems
// 3
let token = queryItems?.first { $0.name == "t
oken" }?.value
// 4
Auth().token = token
// 5
DispatchQueue.main.async {
let appDelegate =
UIApplication.shared.delegate as? AppDel
egate
appDelegate?.window?.rootViewController =
UIStoryboard(name: "Main", bundle: Bundl
e.main)
.instantiateInitialViewController()
}
Here’s what’s going on:
Finally, below
ASWebAuthenticationSession(url:callbackURLSchem
e:) add the following:
session.presentationContextProvider = self
session.start()
import ImperialGitHub
GITHUB_CALLBACK_URL=http://localhost:8080/oa
uth/github
GITHUB_CLIENT_ID=<YOUR_GITHUB_CLIENT_ID>
GITHUB_CLIENT_SECRET=<YOUR_GITHUB_CLIENT_SEC
RET>
// 3
let githubUserAPIURL: URI = "https://a
pi.github.com/user"
// 4
return request
.client
.get(githubUserAPIURL, headers: head
ers)
.flatMapThrowing { response in
// 5
guard response.status == .ok else
{
// 6
if response.status == .unauthori
zed {
throw Abort.redirect(to: "/log
in-github")
} else {
throw Abort(.internalServerErr
or)
}
}
// 7
return try response.content
.decode(GitHubUserInfo.self)
}
}
}
<a href="/login-github">
<img class="mt-3" src="/images/sign-in-wit
h-github.png"
alt="Sign In With GitHub">
</a>
3. Create an instance of
ASWebAuthenticationSession using the
scheme and URL.
4. Ensure there’s a callback URL and no error.
Extract the token from the callback URL.
Build and run the app and log out in the Users
tab if necessary. On the log in page, you’ll see
the new Sign in with GitHub button:
Tap the new button and the app asks you to
confirm that you want to use TILApp to log in:
Tap Continue. If you’re already logged into
GitHub on the simulator, the app logs you
straight in. Otherwise you’ll see the OAuth
screen for GitHub to allow access:
Log in to GitHub and if you’ve approved TILApp
the app logs you in. Otherwise GitHub asks you
to confirm access for the TIL app, just like the
website. Once confirmed, the app logs you in.
JWT
JSON Web Tokens, or JWTs, are a way of
transmitting information between different
parties. Since they contain JSON, you can send
any information you want in them. The issuer of
the JWT signs the token with a private key or
secret. The JWT contains a signature and
header. Using these two pieces, you can verify
the integrity of the token. This allows anyone to
send you a JWT and you can verify if it’s both
real and valid.
.package(
url: "https://github.com/vapor-community/I
mperial.git",
from: "1.0.0"),
.package(
url: "https://github.com/vapor/jwt.git",
from: "4.0.0")
@OptionalField(key: "siwaIdentifier")
var siwaIdentifier: String?
.field("siwaIdentifier", .string)
extension LoginTableViewController:
ASAuthorizationControllerPresentationConte
xtProviding {
func presentationAnchor(
for controller: ASAuthorizationControl
ler
) -> ASPresentationAnchor {
guard let window = view.window else {
fatalError("No window found in vie
w")
}
return window
}
}
// 3
func authorizationController(
controller: ASAuthorizationController,
didCompleteWithError error: Error
) {
print("Error signing in with Apple - \
(error)")
}
}
1. Conforms LoginTableViewController to
ASAuthorizationControllerDelegate. This
handles success and failure cases for
signing in with Apple.
2. Implement
authorizationController(controller:didCompl
eteWithAuthorization:) as required by the
protocol. The app calls this when the device
authenticates the user.
3. Implement
authorizationController(controller:didCompl
eteWithError:) to handle the case when
signing in with Apple fails. For now, just
print the error to the console.
// 1
let request = ASAuthorizationAppleIDProvider
().createRequest()
request.requestedScopes = [.fullName, .emai
l]
// 2
let authorizationController =
ASAuthorizationController(authorizationReq
uests: [request])
// 3
authorizationController.delegate = self
authorizationController.presentationContextP
rovider = self
// 4
authorizationController.performRequests()
Here’s what the new code does:
1. Create an ASAuthorizationAppleIDRequest
with the scopes for a user’s full name and
email.
Next, in
authorizationController(controller:didCompleteW
ithAuthorization:) add the following:
// 1
if let credential = authorization.credential
as? ASAuthorizationAppleIDCredential {
// 2
guard
let identityToken = credential.identityT
oken,
let tokenString = String(
data: identityToken,
encoding: .utf8)
else {
print("Failed to get token from credenti
al")
return
}
// 3
let name: String?
if let nameProvided = credential.fullName
{
let firstName = nameProvided.givenName ??
""
let lastName = nameProvided.familyName
?? ""
name = "\(firstName) \(lastName)"
} else {
name = nil
}
// 4
let requestData =
SignInWithAppleToken(token: tokenString,
name: name)
do {
// 5
try Auth().login(
signInWithAppleInformation: requestDat
a
) { result in
switch result {
// 6
case .success:
DispatchQueue.main.async {
let appDelegate =
UIApplication.shared.delegate a
s? AppDelegate
appDelegate?.window?.rootViewContr
oller =
UIStoryboard(name: "Main", bundl
e: Bundle.main)
.instantiateInitialViewControl
ler()
}
// 7
case .failure:
let message = "Could not Sign in wit
h Apple."
ErrorPresenter.showError(message: me
ssage, on: self)
}
}
// 8
} catch {
let message = "Could not login - \(erro
r)"
ErrorPresenter.showError(message: messag
e, on: self)
}
}
5. Use
login(signInWithAppleInformation:completion
:)to send the JWT to the server and get a
token back.
6. If the login succeeds, change the root view
controller to the main screen as before.
IOS_APPLICATION_IDENTIFIER=<YOUR_BUNDLE_ID>
Then, in Terminal, reset the database to
accommodate the new field on User:
docker rm -f postgres
docker run --name postgres \
-e POSTGRES_DB=vapor_database \
-e POSTGRES_USER=vapor_username \
-e POSTGRES_PASSWORD=vapor_password \
-p 5432:5432 -d postgres
Setting up ngrok
Sign in with Apple on the web only works with
HTTPS connections, and Apple will only redirect
to an HTTPS address. This is fine for deploying,
but makes testing locally harder. ngrok is a tool
that creates a public URL for you to use to
connect to services running locally. In your
browser, visit https://ngrok.com and download
the client and create an account.
Note: You can also install ngrok with
Homebrew.
authSessionsRoutes.post(
"login",
"siwa",
"callback",
use: appleAuthCallbackHandler)
import Fluent
authSessionsRoutes.post(
"login",
"siwa",
"handle",
use: appleAuthRedirectHandler)
<!-- 1 -->
<div id="appleid-signin" class="signin-butto
n"
data-color="black" data-border="true"
data-type="sign in"></div>
<!-- 2 -->
<script type="text/javascript"
src="https://appleid.cdn-apple.com/appleaut
h/static/jsapi/appleid/1/en_US/appleid.auth.
js"></script>
<!-- 3 -->
<script type="text/javascript">
AppleID.auth.init({
clientId : '#(siwaContext.clientID)',
scope : '#(siwaContext.scopes)',
redirectURI : '#(siwaContext.redirectUR
I)',
state : '#(siwaContext.state)',
usePopup : false
});
</script>
undefined
WEBSITE_APPLICATION_IDENTIFIER=<YOUR_WEBSITE
_IDENTIFIER>
SIWA_REDIRECT_URL=https://<YOUR_NGROK_DOMAIN
>/login/siwa/callback
@Field(key: "email")
var email: String
Web registration
One method of creating users in the TIL app is
registering through the website. Open
WebsiteController.swift and add the following
property to the bottom of RegisterData:
after:
validations.add(
"zipCode",
as: String.self,
is: .zipCode,
required: false)
<div class="form-group">
<label for="emailAddress">Email Address</l
abel>
<input type="email" name="emailAddress" cl
ass="form-control"
id="emailAddress"/>
</div>
Fixing GitHub
Getting the email address for a GitHub user is
more complicated. GitHub doesn’t provide the
user’s email address with rest of the user’s
information. You must get the email address in a
second request.
First, in ImperialController.swift in
boot(routes:), replace try routes.oAuth(from:
GitHub.self, ...) with the following:
try routes.oAuth(
from: GitHub.self,
authenticate: "login-github",
callback: githubCallbackURL,
scope: ["user:email"],
completion: processGitHubLogin)
// 3
let githubUserAPIURL: URI =
"https://api.github.com/user/emails"
return request.client
.get(githubUserAPIURL, headers: header
s)
.flatMapThrowing { response in
// 4
guard response.status == .ok else {
// 5
if response.status == .unauthorize
d {
throw Abort.redirect(to: "/login
-github")
} else {
throw Abort(.internalServerErro
r)
}
}
// 6
return try response.content
.decode([GitHubEmailInfo].self)
}
}
Finally, replace
processGitHubLogin(request:token) with the
following:
func processGitHubLogin(request: Request, to
ken: String) throws
-> EventLoopFuture<ResponseEncodable> {
// 1
return try GitHub.getUser(on: request)
.and(GitHub.getEmails(on: request))
.flatMap { userInfo, emailInfo in
return User.query(on: request.db)
.filter(\.$username == userInfo.log
in)
.first()
.flatMap { foundUser in
guard let existingUser = foundUs
er else {
// 2
let user = User(
name: userInfo.name,
username: userInfo.login,
password: UUID().uuidString,
email: emailInfo[0].email)
return user.save(on: request.d
b).flatMap {
request.session.authenticate
(user)
return generateRedirect(on:
request, for: user)
}
}
request.session.authenticate(exi
stingUser)
return generateRedirect(
on: request,
for: existingUser)
}
}
}
userToLogin = User(
name: "Admin",
username: "admin",
password: "password",
email: "[email protected]")
Make sure you have your .env file that you built
over the past three chapters and that you have
set a custom working directory in Xcode. Then,
run the tests and they should all pass.
docker rm -f postgres
docker run --name postgres \
-e POSTGRES_DB=vapor_database \
-e POSTGRES_USER=vapor_username \
-e POSTGRES_PASSWORD=vapor_password \
-p 5432:5432 -d postgres
Open CreateUserTableViewController.swift in
the Assistant editor. Create an IBOutlet for the
user’s email text field below @IBOutlet weak var
passwordTextField: UITextField! by Control-
dragging to CreateUserTableViewController. Name
the outlet emailTextField.
guard
let email = emailTextField.text,
!email.isEmpty
else {
ErrorPresenter
.showError(message: "You must specify an
email", on: self)
return
}
Integrating SendGrid
Finally, you’ve added an email address to the
user model! Now it’s time to learn how to send
emails. This chapter uses SendGrid for that
purpose. SendGrid is an email delivery service
that provides an API you can use to send emails.
It has a free tier allowing you to send 100 emails
a day at no cost. There’s also a community
package — https://github.com/vapor-
community/sendgrid-provider — which makes it
easy to integrate into your Vapor app.
.package(
url: "https://github.com/vapor/jwt.git",
from: "4.0.0"),
.package(
url: "https://github.com/vapor-community/s
endgrid.git",
from: "4.0.0")
Next, add the dependency to your App target’s
dependency array. Replace .product(name: "JWT",
package: "jwt"), with:
import SendGrid
Next, add the following below try routes(app):
app.sendgrid.initialize()
SENDGRID_API_KEY=<YOUR_API_KEY>
// 1
func forgottenPasswordHandler(_ req: Reques
t)
-> EventLoopFuture<View> {
// 2
req.view.render(
"forgottenPassword",
["title": "Reset Your Password"])
}
authSessionsRoutes.get(
"forgottenPassword",
use: forgottenPasswordHandler)
<!-- 4 -->
<form method="post">
<div class="form-group">
<label for="email">Email</label>
<!-- 5 -->
<input type="email" name="email" cla
ss="form-control"
id="email"/>
</div>
<!-- 6 -->
<button type="submit" class="btn btn-p
rimary">
Reset Password
</button>
</form>
#endexport
#endextend
<br />
<a href="/forgottenPassword">Forgotten your
password?</a>
authSessionsRoutes.post(
"forgottenPassword",
use: forgottenPasswordPostHandler)
#extend("base"):
#export("content"):
<h1>#(title)</h1>
@ID
var id: UUID?
@Field(key: "token")
var token: String
@Parent(key: "userID")
var user: User
init() {}
import Fluent
app.migrations.add(CreateResetPasswordToken
())
Sending emails
Return to WebsiteController.swift. At the top of
the file, insert the following below import
Fluent:
import SendGrid
Then, in forgottenPasswordPostHandler(_:),
replace
req.view.render("forgottenPasswordConfirmed")
with the following:
// 1
guard let user = user else {
return req.view.render(
"forgottenPasswordConfirmed",
["title": "Password Reset Email Sent"])
}
// 2
let resetTokenString =
Data([UInt8].random(count: 32)).base32Enco
dedString()
// 3
let resetToken: ResetPasswordToken
do {
resetToken = try ResetPasswordToken(
token: resetTokenString,
userID: user.requireID())
} catch {
return req.eventLoop.future(error: error)
}
// 4
return resetToken.save(on: req.db).flatMap {
// 5
let emailContent = """
<p>You've requested to reset your passwor
d. <a
href="http://localhost:8080/resetPassword?
\
token=\(resetTokenString)">
Click here</a> to reset your password.</p>
"""
// 6
let emailAddress = EmailAddress(
email: user.email,
name: user.name)
let fromEmail = EmailAddress(
email: "<SENDGRID SENDER EMAIL>",
name: "Vapor TIL")
// 7
let emailConfig = Personalization(
to: [emailAddress],
subject: "Reset Your Password")
// 8
let email = SendGridEmail(
personalizations: [emailConfig],
from: fromEmail,
content: [
["type": "text/html",
"value": emailContent]
])
// 9
let emailSend: EventLoopFuture<Void>
do {
emailSend =
try req.application
.sendgrid
.client
.send(email: email, on: req.eventLoo
p)
} catch {
return req.eventLoop.future(error: erro
r)
}
return emailSend.flatMap {
// 10
return req.view.render(
"forgottenPasswordConfirmed",
["title": "Password Reset Email Sent"]
)
}
}
In boot(routes:), below
authSessionsRoutes.post("forgottenPassword",
use: forgottenPasswordPostHandler), register the
new route:
authSessionsRoutes.get(
"resetPassword",
use: resetPasswordHandler)
<!-- 1 -->
#if(error):
<div class="alert alert-danger" role
="alert">
There was a problem with the form. E
nsure you clicked on
the full link with the token and you
r passwords match.
</div>
#endif
<!-- 2 -->
<form method="post">
<!-- 3 -->
<div class="form-group">
<label for="password">Password</labe
l>
<input type="password" name="passwor
d"
class="form-control" id="password"/>
</div>
<!-- 4 -->
<div class="form-group">
<label for="confirmPassword">Confirm P
assword</label>
<input type="password" name="confirmP
assword"
class="form-control" id="confirmPassw
ord"/>
</div>
<!-- 5 -->
<button type="submit" class="btn btn-p
rimary">
Reset
</button>
</form>
#endexport
#endextend
init(
name: String,
username: String,
password: String,
siwaIdentifier: String? = nil,
email: String,
profilePicture: String? = nil
) {
self.name = name
self.username = username
self.password = password
self.siwaIdentifier = siwaIdentifier
self.email = email
self.profilePicture = profilePicture
}
docker rm -f postgres
docker rm -f postgres-test
docker run --name postgres -e POSTGRES_DB=va
por_database \
-e POSTGRES_USER=vapor_username \
-e POSTGRES_PASSWORD=vapor_password \
-p 5432:5432 -d postgres
docker run --name postgres-test -e POSTGRES_
DB=vapor-test \
-e POSTGRES_USER=vapor_username \
-e POSTGRES_PASSWORD=vapor_password \
-p 5433:5432 -d postgres
docker ps -a
protectedRoutes.get(
"users",
":userID",
"addProfilePicture",
use: addProfilePictureHandler)
This connects a GET request to
/users/<USER_ID>/addProfilePicture to
addProfilePictureHandler(_:). Note that the route
is also a protected route — users must be logged
in to add profiles pictures to users.
<!-- 4 -->
<form method="post" enctype="multipart/f
orm-data">
<!-- 5 -->
<div class="form-group">
<label for="picture">
Select Picture for #(username)
</label>
<input type="file" name="picture"
class="form-control-file" id="pictur
e"/>
</div>
<!-- 6 -->
<button type="submit" class="btn btn-p
rimary">
Upload
</button>
</form>
#endexport
#endextend
// 1
let loggedInUser = req.auth.get(User.self)
// 2
let context = UserContext(
title: user.name,
user: user,
acronyms: acronyms,
authenticatedUser: loggedInUser)
#if(authenticatedUser):
<a href="/users/#(user.id)/addProfilePictur
e">
#if(user.profilePicture):
Update
#else:
Add
#endif
Profile Picture
</a>
#endif
# 1
mkdir ProfilePictures
# 2
touch ProfilePictures/.keep
protectedRoutes.on(
.POST,
"users",
":userID",
"addProfilePicture",
body: .collect(maxSize: "10mb"),
use: addProfilePicturePostHandler)
authSessionsRoutes.get(
"users",
":userID",
"profilePicture",
use: getUsersProfilePictureHandler)
Modifying tables
Modifying an existing database is always a risky
business. You already have data you don’t want
to lose, so deleting the whole database is not a
viable solution. At the same time, you can’t
simply add or remove a property in an existing
table since all the data is entangled in one big
web of connections and relations.
Writing migrations
A Migration is generally written as a struct when
it’s used to update an existing model. This
struct must, of course, conform to Migration.
Migration requires you to provide two things:
func prepare(on database: Database) -> Event
LoopFuture<Void>
Prepare method
Migrations require a database connection to
work correctly as they must be able to query the
MigrationLog model. If the MigrationLog is not
accessible, the migration will fail and, in the
worst case, break your application. prepare(on:)
contains the migration’s changes to the
database. It’s usually one of two options:
Revert method
revert(on:) is the opposite of prepare(on:). Its
job is to undo whatever prepare(on:) did. If you
use create() in prepare(on:), you use delete() in
revert(on:). If you use update() to add a field,
you also use it in revert(on:) to remove the field
with deleteField(_:).
FieldKeys
In Vapor 3, Fluent inferred most of the table
information for you. This included the column
types and the names of the columns. This
worked well for small apps such as the TIL app.
However, as projects grow, they make more and
more changes. Removing fields and changing
names of columns was difficult because the
columns no longer matched the model. Fluent 4
makes migrations a lot more flexible by
requiring you to provide the names of fields and
schemas.
However, this means you end up duplicating
strings throughout your app, a technique which
is prone to mistakes. You can define your own
FieldKeys to work around this. In Xcode, open
CreateAcronym.swift and add the following at
the bottom of the file:
extension Acronym {
// 1
enum v20210114 {
// 2
static let schemaName = "acronyms"
// 3
static let id = FieldKey(stringLiteral:
"id")
static let short = FieldKey(stringLitera
l: "short")
static let long = FieldKey(stringLitera
l: "long")
static let userID = FieldKey(stringLiter
al: "userID")
}
}
@Field(key: Acronym.v20210114.short)
var short: String
@Field(key: Acronym.v20210114.long)
var long: String
@Parent(key: Acronym.v20210114.userID)
var user: User
Finally, open
CreateAcronymCategoryPivot.swift. Replace:
.field(
AcronymCategoryPivot.v20210113.acronymID,
.uuid,
.required,
.references("acronyms", "id", onDelete: .c
ascade))
.field(
AcronymCategoryPivot.v20210113.acronymID,
.uuid,
.required,
.references(
Acronym.v20210114.schemaName,
Acronym.v20210114.id,
onDelete: .cascade))
enum v20210114 {
static let twitterURL = FieldKey(stringLit
eral: "twitterURL")
}
@OptionalField(key: User.v20210114.twitterUR
L)
var twitterURL: String?
This adds the property of type String? to the
model. You declare it as an optional string since
your existing users don’t have the property and
future users don’t necessarily have a Twitter
account. You annotate the property with
@OptionalField to tell Fluent the property is an
optional field in the database.
init(
id: UUID? = nil,
name: String,
username: String,
password: String,
twitterURL: String? = nil
) {
self.name = name
self.username = username
self.password = password
self.twitterURL = twitterURL
}
// 1
struct AddTwitterURLToUser: Migration {
// 2
func prepare(on database: Database) -> Eve
ntLoopFuture<Void> {
// 3
database.schema(User.v20210113.schemaNam
e)
// 4
.field(User.v20210114.twitterURL, .stri
ng)
// 5
.update()
}
// 6
func revert(on database: Database) -> Even
tLoopFuture<Void> {
// 7
database.schema(User.v20210113.schemaNam
e)
// 8
.deleteField(User.v20210114.twitterUR
L)
// 9
.update()
}
}
app.migrations.add(AddTwitterURLToUser())
init(id: UUID?,
name: String,
username: String,
twitterURL: String? = nil) {
self.id = id
self.name = name
self.username = username
self.twitterURL = twitterURL
}
}
This creates a new PublicV2 class that includes
the twitterURL. Next, create the four convert
methods for the version 2 API. Add the
following to the extension for User after
convertToPublic():
// 1
func getV2Handler(_ req: Request)
-> EventLoopFuture<User.PublicV2> {
// 2
User.find(req.parameters.get("userID"), on:
req.db)
.unwrap(or: Abort(.notFound))
.convertToPublicV2()
}
This method is just like getHandler(_:) with two
changes:
1. Return a User.PublicV2.
<h2>#(user.username)
#if(user.twitterURL):
- @#(user.twitterURL)
#endif
</h2>
With:
import Fluent
// 1
struct MakeCategoriesUnique: Migration {
// 2
func prepare(on database: Database) -> Eve
ntLoopFuture<Void> {
// 3
database.schema(Category.v20210113.schem
aName)
// 4
.unique(on: Category.v20210113.name)
// 5
.update()
}
// 6
func revert(on database: Database) -> Even
tLoopFuture<Void> {
// 7
database.schema(Category.v20210113.schem
aName)
// 8
.deleteUnique(on: Category.v20210113.n
ame)
// 9
.update()
}
}
1. Define a new type, MakeCategoriesUnique,
that conforms to Migration.
app.migrations.add(MakeCategoriesUnique())
app.migrations.add(CreateAdminUser())
switch app.environment {
case .development, .testing:
app.migrations.add(CreateAdminUser())
default:
break
}
// 2
func set<T>(_ key: String, to value: T?) -
> EventLoopFuture<Void>
where T: Encodable
}
In-memory caches
Vapor comes with an in-memory cache: .memory.
This cache stores its data in your program’s
running memory. This makes it great for
development and testing because it has no
external dependencies. However, it may not be
perfect for all uses as the storage is cleared
when the application restarts and can’t be
shared between multiple instances of your
application. Most likely though, this memory
volatility won’t affect a well thought out
caching design.
Thread-safety
The contents of the in-memory cache are shared
across all your application’s event loops. This
means once something is stored in the cache, all
future requests will see that same item
regardless of which event loop they are assigned
to. To achieve this cross-loop sharing, the in-
memory cache uses an application-wide lock to
synchronize access.
Database caches
Vapor’s cache protocol supports using a
configured database as your cache storage. This
includes all of Vapor’s Fluent mappings
(PostgreSQL, MySQL, SQLite, MongoDB, etc.).
Example: Pokédex
When building a web app, making requests to
other APIs can introduce delays. If the API
you’re communicating with is slow, it can make
your API feel slow. Additionally, external APIs
may enforce rate limits on the number of
requests you can make to them in a given time
period.
open Package.swift
Overview
This simple Pokédex API has two routes:
GET /pokemon: Returns a list of all
captured Pokémon.
Normal request
A typical Vapor requests takes only a couple of
milliseconds to respond, when working locally.
In the screenshot that follows, you can see the
GET /pokemon route has a total response time
of about 40ms.
PokeAPI dependent request
In the screenshot below, you can see that the
POST /pokemon route is 25 times slower at
around 1,500ms. This is because the pokeapi.co
API can be slow to respond to the query.
Now you’re ready to take a look at the code to
better understand what’s making this route slow
and how a cache can fix it.
extension Request {
public var pokeAPI: PokeAPI {
.init(client: self.client, cache: self.c
ache)
}
}
By default, Vapor is configured to use the built-
in memory cache.
// 2
return cache.get(name, as: Bool.self).flatM
ap { verified in
// 3
if let verified = verified {
return self.client.eventLoop.makeSucce
ededFuture(verified)
} else {
return self.uncachedVerify(name: nam
e).flatMap {
verified in
// 4
return self.cache.set(name, to: veri
fied)
.transform(to: verified)
}
}
}
}
URL: http://localhost:8080/pokemon
method: POST
name: Test
Fluent
Once you have configured your app to use
Vapor’s cache interface, it’s easy to swap out the
underlying implementation. Since this app
already uses SQLite to store caught Pokémon,
you can easily enable Fluent as a cache. Unlike
in-memory caching, Fluent caches are shared
between multiple instances of your application
and are persisted between restarts.
app.caches.use(.fluent)
try routes(app)
app.migrations.add(CreatePokemon())
Vapor’s middleware
Vapor includes some middleware out of the box.
This section introduces you to the available
options to give you an idea of what middleware
is commonly used for.
Error middleware
The most commonly used middleware in Vapor
is ErrorMiddleware. It’s responsible for converting
both synchronous and asynchronous Swift
errors into HTTP responses. Uncaught errors
cause the HTTP server to immediately close the
connection and print an internal error log.
File middleware
Another common type of middleware is
FileMiddleware. This middleware serves files
from the Public folder in your application
directory. This is useful when you’re using Vapor
to create a front-end website that may require
static files like images or style sheets.
Other Middleware
Vapor also provides a SessionsMiddleware,
responsible for tracking sessions with connected
clients. Other packages may provide middleware
to help them integrate into your application. For
example, Vapor’s Authentication package
contains middleware for protecting your routes
using basic passwords, simple bearer tokens,
and even JWTs (JSON Web Tokens).
Example: Todo API
Now that you have an understanding of how
various types of middleware function, you’re
ready to learn how to configure them and how
to create your own custom middleware types.
Log middleware
The first middleware you’ll create will log
incoming requests. It will display the following
information for each request:
Request method
Request path
Response status
open Package.swift
app.middleware.use(LogMiddleware())
curl localhost:8080/todos
// 3
func log(_ res: Response, start: Date, for r
eq: Request) {
let reqInfo = "\(req.method.string) \(req.
url.path)"
let resInfo = "\(res.status.code) " +
"\(res.status.reasonPhrase)"
// 4
let time = Date()
.timeIntervalSince(start)
.readableMilliseconds
// 5
req.logger.info("\(reqInfo) -> \(resInfo)
[\(time)]")
}
curl localhost:8080/todos
Secret middleware
Now that you’ve learned how to create
middleware and apply it globally, you’ll learn
how to apply middleware to specific routes.
POST /todos
DELETE /todos/:id
init(secret: String) {
self.secret = secret
}
// 2
func respond(
to request: Request,
chainingTo next: Responder
) -> EventLoopFuture<Response> {
// 3
guard
request.headers.first(name: .xSecret) =
= secret
else {
// 4
return request.eventLoop.makeFailedFut
ure(
Abort(
.unauthorized,
reason: "Incorrect X-Secret heade
r."))
}
// 5
return next.respond(to: request)
}
}
Here’s a breakdown of how SecretMiddleware
works:
// 1
try app.group(SecretMiddleware.detect()) { s
ecretGroup in
// 2
secretGroup.post("todos", use: todoControl
ler.create)
secretGroup.delete(
"todos",
":id",
use: todoController.delete)
}
SECRET=foo
URL: http://localhost:8080/todos
method: POST
Tools
Testing WebSockets can be a bit tricky since
they can send/receive multiple messages. This
makes using a simple CURL request or a browser
difficult. Fortunately, there’s a great WebSocket
client tool you can use to test your server at:
https://www.websocketking.com. It’s important
to note that, as of writing this, connections to
localhost are only supported in Chrome.
A basic server
Now that your tools are ready, it’s time to set up
a very basic WebSocket server. Copy this
chapter’s starter project to your favorite
location and open a Terminal window in that
directory.
cd share-touch-server
open Package.swift
Echo server
Open WebSockets.swift and add the following to
the end of sockets(_:) to create an echo
endpoint:
// 1
app.webSocket("echo") { req, ws in
// 2
print("ws connected")
// 3
ws.onText { ws, text in
// 4
print("ws received: \(text)")
// 5
ws.send("echo: " + text)
}
}
Connected to ws://localhost:8080/echo
Connecting to ws://localhost:8080/echo
Joined
A new participant will open a WebSocket using
the /session endpoint. In the opening request,
you’ll include two bits of information from the
user: the color to use — represented as r,g,b,a —
and a starting point — represented using a
relative point.
Moved
To keep things simple, after a client opens a new
session, the only thing it will send the server is
new relative points as the user drags the circle.
Left
This server will interpret any closure on the
client’s side as leaving the room. This keeps
things succinct.
Joined
When the server sends a joined message, it
includes in the message an ID, a Color and the
last known point for that participant.
Moved
Any time a participant moves, the server notifies
the clients. These notifications include only an
ID and a new relative point.
Left
Any time a participant disconnects from the
session, the server notifies all other participants
and removes that user from associated views.
Setting up “Join”
Open WebSockets.swift and add the following to
the end of sockets(_:)
// 1
app.webSocket("session") { req, ws in
// 2
ws.onText { ws, text in
print("got message: \(text)")
}
}
iOS project
The materials for this chapter include a
complete iOS app. You can change the URL
you’d like to use in ShareTouchApp.swift. For
now, it should be set to
ws://localhost:8080/session. Build and run the
app in the simulator. Select a color and press
BEGIN, then drag the circle around the screen.
You should see logs in your server application
that look similar to the following:
Finishing “Join”
As described earlier, the client will include a
color and a starting position in the web socket
connection request. WebSocket requests are
treated as an upgraded GET request, so you’ll
include the data in the query of the request. In
WebSockets.swift, replace the code you added
earlier for app.webSocket("session") with the
following:
app.webSocket("session") { req, ws in
// 1
let color: ColorComponents
let position: RelativePoint
do {
color = try req.query.decode(ColorCompon
ents.self)
position = try req.query.decode(Relative
Point.self)
} catch {
// 2
_ = ws.close(code: .unacceptableData)
return
}
// 3
print("new user joined with: \(color) at \
(position)")
}
Handling “Moved”
Next, you need to listen to messages from the
client. For now, you’ll only expect to receive a
stream of RelativePoint objects. In this case,
you’ll use onText(_:). Using onText(_:) is perhaps
slightly less performant than using onBinary(_:)
and receiving data directly. However, it makes
debugging easier and you can change it later.
Below TouchSessionManager.default.insert(id:
newId, color: color, at: position, on: ws) add
the following:
// 1
ws.onText { ws, text in
do {
// 2
let pt = try JSONDecoder()
.decode(RelativePoint.self, from: Data
(text.utf8))
// 3
TouchSessionManager.default.update(id: n
ewId, to: pt)
} catch {
// 4
ws.send("unsupported update: \(text)")
}
}
// 1
_ = ws.onClose.always { result in
// 2
TouchSessionManager.default.remove(id: new
Id)
}
Implementing
TouchSessionManager: Joined
At this point, you can successfully dispatch
WebSocket events to their associated
architecture event in the TouchSessionManager.
Next, you need to implement the management
logic. Open TouchSessionManager.swift and
replace the body of insert(id:color:at:on:) with
the following:
// 1
let start = SharedTouch(
id: id,
color: color,
position: pt)
let msg = Message(
participant: id,
update: .joined(start))
// 2
send(msg)
// 3
participants.values.map {
Message(
participant: $0.touch.participant,
update: .joined($0.touch))
} .forEach { ws.send($0) }
Implementing
TouchSessionManager: Moved
Next, to handle “moved” messages, replace the
body of update(id:to:) with the following code:
// 1
participants[id]?.touch.position = pt
// 2
let msg = Message(participant: id, update: .
moved(pt))
// 3
send(msg)
Implementing
TouchSessionManager: Left
Finally, you need to handle closes and
cancellations. Replace the body of remove(id:)
with the following:
// 1
participants[id] = nil
// 2
let msg = Message(participant: id, update: .
left)
// 3
send(msg)
Challenges
For more practice with WebSockets, try these
challenges:
Getting started
The starter project for this chapter is based on
the TIL application from the end of chapter 21.
You can either use your code from that project
or use the starter project included in the book
materials for this chapter. This project relies on
a PostgreSQL database running locally.
Clearing the existing database
If you’ve followed along from the previous
chapters, you need to delete the existing
database. This chapter contains model changes
which require either reverting your database or
deleting it. In Terminal, type:
docker rm -f postgres
Soft delete
In Chapter 7, “CRUD Database Operations”, you
learned how to delete models from the database.
However, while you may want models to appear
deleted to users, you might not want to actually
delete them. You could also have legal or
company requirements which enforce retention
of data. Fluent provides soft delete functionality
to allow you to do this. Open the TIL app in
Xcode and go to User.swift. Look for:
URL: http://localhost:8080/api/users
method: POST
URL:
http://localhost:8080/api/users/<USER_ID>
method: DELETE
URL: http://localhost:8080/api/users/
method: GET
import Fluent
tokenAuthGroup.post(":userID", "restore", us
e: restoreHandler)
URL:
http://localhost:8080/api/users/<USER_ID>/
restore
method: POST
method: GET
Force delete
Now that you can soft delete and restore users,
you may want to add the ability to properly
delete a user. You use force delete for this. Back
in Xcode, still in UsersController.swift, create a
new route to do this. Add the following below
restoreHandler(_:):
tokenAuthGroup.delete(
":userID",
"force",
use: forceDeleteHandler)
This routes a DELETE request to
/api/users/<USER_ID>/force to
forceDeleteHandler(_:). Build and run the
application and go back to RESTed. Configure a
new request as follows:
URL:
http://localhost:8080/api/users/<USER_ID>/
force
method: DELETE
URL:
http://localhost:8080/api/users/<USER_ID>/
restore
method: POST
Timestamps
Fluent has built-in functionality for timestamps
for a model’s creation time and update time. In
fact, you used one above to implement soft-
delete functionality. If you configure these,
Fluent automatically sets and updates the times.
To enable this, open Acronym.swift in Xcode.
Below var categories: [Category] add two new
properties for the dates:
.field("created_at", .datetime)
.field("updated_at", .datetime)
docker rm -f postgres
docker run --name postgres \
-e POSTGRES_DB=vapor_database \
-e POSTGRES_USER=vapor_username \
-e POSTGRES_PASSWORD=vapor_password \
-p 5432:5432 -d postgres
URL:
http://localhost:8080/api/acronyms/<ID_OF
_FIRST_ACRONYM>
method: PUT
URL:
http://localhost:8080/api/acronyms/mostRe
cent
method: GET
// 1
enum UserType: String, Codable {
// 2
case admin
case standard
case restricted
}
@Enum(key: "userType")
var userType: UserType
init(
id: UUID? = nil,
name: String,
username: String,
password: String,
userType: UserType = .standard
) {
self.name = name
self.username = username
self.password = password
self.userType = userType
}
// 1
database.enum("userType")
// 2
.case("admin")
.case("standard")
.case("restricted")
// 3
.create()
.flatMap { userType in
database.schema("users")
.id()
.field("name", .string, .required)
.field("username", .string, .required)
.field("password", .string, .required)
.field("deleted_at", .datetime)
// 4
.field("userType", userType, .required)
.unique(on: "username")
.create()
}
URL: http://localhost:8080/api/users
method: POST
userType: standard
URL:
http://localhost:8080/api/users/<FINAL_US
ER_ID>
method: DELETE
Lifecycle hooks
Fluent allows you to hook into various aspects
of a model’s lifecycle using model middleware.
These work in a similar way to other middleware
and allow you to execute code before and after
different events. For more information on
middleware, see Chapter 29, “Middleware”.
Fluent allows you to add middleware for the
following events:
// 1
struct UserMiddleware: ModelMiddleware {
// 2
func create(
model: User,
on db: Database,
next: AnyModelResponder) -> EventLoopFut
ure<Void> {
// 3
User.query(on: db)
.filter(\.$username == model.username)
.count()
.flatMap { count in
// 4
guard count == 0 else {
let error =
Abort(
.badRequest,
reason: "Username already exis
ts")
return db.eventLoop.future(error:
error)
}
// 5
return next.create(model, on: db).ma
p {
// 6
let errorMessage: Logger.Message =
"Created user with username \(mo
del.username)"
db.logger.debug(errorMessage)
}
}
}
}
2. Implement create(model:on:next:) to
perform additional checks before you create
a user.
app.databases.middleware.use(UserMiddleware
(), on: .psql)
This registers UserMiddleware to psql to ensure it
runs whenever you create a User.
URL: http://localhost:8080/api/users
method: POST
username: admin
name: Admin
password: password
userType: admin
categoriesRoute.get(
"acronyms",
use: getAllCategoriesWithAcronymsAndUsers)
URL:
http://localhost:8080/api/categories/acrony
ms
method: GET
Joins
Sometimes, you want to query other tables
when retrieving information. For example, you
might want to get the user who created the most
recent acronym. You could do this with eager
loading and Swift. You’d do this by getting all
the users and eager load their acronyms. You
can then sort the acronyms by their created date
to get the most recent and return its user.
However, this means loading all users and their
acronyms into memory, even if you don’t want
them, which is inefficient. Joins allow you to
combine columns from one table with columns
from another table by specifying the common
values. For example, you can combine the
acronyms table with the users table using the
users’ IDs. You can then sort, or even filter,
across the different tables.
usersRoute.get(
"mostRecentAcronym",
use: getUserWithMostRecentAcronym)
URL:
http://localhost:8080/api/users/mostRecent
Acronym
method: GET
Click Send Request and you’ll see the user who
created the most recent acronym:
Raw SQL
Whilst Fluent provides tools to allow you to
build lots of different behaviors, there are some
advanced features it doesn’t offer. Fluent
doesn’t support querying different schemas or
aggregate functions. In a complex application,
you may find that there are scenarios where
Fluent doesn’t provide the functionality you
need. In these cases, you can use raw SQL
queries to interact with the database directly.
This allows you to perform any type of query the
database supports.
import SQLKit
URL:
http://localhost:8080/api/acronyms/raw
method: GET
Setting up Heroku
If you don’t already have a Heroku account, sign
up for one now. Heroku offers free options and
setting up an account is painless. Simply visit
https://signup.heroku.com/ and follow the
instructions to create an account.
Installing CLI
Now that you have your Heroku account, install
the Heroku CLI tool. The easiest way to install
on macOS is through Homebrew. In Terminal,
enter:
Logging in
With the Heroku CLI installed, you need to log
in to your account. In Terminal, enter:
heroku login
heroku auth:whoami
That’s it; Heroku is all set up on your system.
Now it’s time to create your first project.
Create an application
Visit heroku.com in your browser to create a
new application. Heroku.com should redirect
you to dashboard.heroku.com. If it doesn’t,
make sure you’re logged in and try again. Once
at the dashboard, in the upper right hand
corner, there’s a button that says New. Click it
and select Create new app.
Git
Heroku uses Git to deploy your app, so you’ll
need to put your project into a Git repository, if
it isn’t already.
git init
git add .
git commit -m "Initial commit"
Branch
Heroku deploys the main branch. Make sure you
are on this branch and have merged any changes
you wish to deploy.
git branch
* main
commander
other-branches
git add .
git commit -m "a description of the changes
I made"
Set Buildpack
Heroku uses something called a Buildpack to
provide the recipe for building your app when
you deploy it. The Vapor Community currently
provides a Buildpack designed for Vapor apps.
To set the Buildpack for your application, enter
the following in Terminal:
heroku buildpacks:set \
https://github.com/vapor-community/heroku-
buildpack
Procfile
Once the app is built on Heroku, Heroku needs
to know what type of process to run and how to
run it. To determine this, it utilizes a special file
named Procfile. Enter the following command to
create your Procfile:
Commit changes
As mentioned earlier, Heroku uses Git and the
main branch to deploy applications. Since you
configured Git earlier, you’ve added two files:
Procfile and .swift-version. These need to be
committed before deploying or Heroku won’t be
able to properly build the application. Enter the
following commands in Terminal:
git add .
git commit -m "adding heroku build files"
In Terminal, enter:
heroku config
postgres://cybntsgadydqzm:2d9dc7f6d964f4750d
a1518ad71hag2ba729cd4527d4a18c70e024b11cfa8f
[email protected]
m:5432/dfr89mvoo550b4
app.databases.use(.postgres(
hostname: Environment.get("DATABASE_HOST")
?? "localhost",
port: databasePort,
username: Environment.get("DATABASE_USERNA
ME") ??
"vapor_username",
password: Environment.get("DATABASE_PASSWO
RD") ??
"vapor_password",
database: Environment.get("DATABASE_NAME")
?? databaseName
), as: .psql)
git add .
git commit -m "configured heroku database"
heroku config:set \
GOOGLE_CALLBACK_URL=https://<YOUR_HEROKU_U
RL>/oauth/google
Deploy to Heroku
You’re now ready to deploy your app to Heroku.
Push your main branch to your Heroku remote
and wait for everything to build. This can take a
while, particularly on a large application.
To kick things off, enter the following in
Terminal:
heroku open
Where to go from here?
In this chapter, you learned how to set up the
app in the Heroku dashboard, configure your Git
repository, add the necessary configuration files
to your project, and deploy your app. Explore
your dashboard and the Heroku Help to learn
even more options!
Chapter 33: Deploying
with Docker
Docker is a popular containerization technology
that has made a huge impact in the way
applications are deployed. Containers are a way
of isolating your applications, allowing you to
run multiple applications on the same server.
Docker Compose
This chapter will also show you how to use
Docker Compose. Docker Compose is a way to
specify a list of different containers that work
together as a single unit. These containers share
the same virtual network, making it simple for
them cooperate with each other.
# 10
start_dependencies:
image: dadarek/wait-for-dependencies
depends_on:
- postgres
command: postgres:5432
docker-compose -f docker-compose-develop.yml
down
docker volume prune -f
# 2
RUN export DEBIAN_FRONTEND=noninteractive DE
BCONF_NONINTERACTIVE_SEEN=true \
&& apt-get -q update \
&& apt-get -q dist-upgrade -y \
&& rm -rf /var/lib/apt/lists/*
# 3
WORKDIR /build
# 4
COPY ./Package.* ./
RUN swift package resolve
# 5
COPY . .
RUN swift build --enable-test-discovery -c r
elease
# 6
WORKDIR /staging
RUN cp "$(swift build --package-path /build
-c release \
--show-bin-path)/Run" ./
RUN [ -d /build/Public ] && \
{ mv /build/Public ./Public && chmod -R
a-w ./Public; } \
|| true
RUN [ -d /build/Resources ] && \
{ mv /build/Resources ./Resources && \
chmod -R a-w ./Resources; } || true
# 7
FROM swift:5.3-focal-slim
# 8
RUN export DEBIAN_FRONTEND=noninteractive \
DEBCONF_NONINTERACTIVE_SEEN=true && \
apt-get -q update && \
apt-get -q dist-upgrade -y && \
rm -r /var/lib/apt/lists/*
# 9
RUN useradd --user-group --create-home --sys
tem \
--skel /dev/null --home-dir /app vapor
# 10
WORKDIR /app
# 11
COPY --from=build --chown=vapor:vapor /stagi
ng /app
# 12
USER vapor:vapor
# 13
EXPOSE 8080
# 14
ENTRYPOINT ["./Run"]
CMD ["serve", "--env", "production", "--host
name",
"0.0.0.0", "--port", "8080"]
1. Use version 5.3 of the “swift” image from
the Docker Hub repository as the starting
point. This container is only for building
your app and you may delete it once Docker
builds the app.
# 2
volumes:
db_data:
# 3
x-shared_environment: &shared_environment
LOG_LEVEL: ${LOG_LEVEL:-debug}
DATABASE_HOST: db
DATABASE_NAME: vapor_database
DATABASE_USERNAME: vapor_username
DATABASE_PASSWORD: vapor_password
# 4
services:
# 5
app:
# 6
image: tilapp:latest
# 7
build:
context: .
# 8
environment:
<<: *shared_environment
# 9
depends_on:
- db
# 10
ports:
- '8080:8080'
# 11
command: ["serve", "--env", "productio
n", "--hostname",
"0.0.0.0", "--port", "8080"]
# 12
db:
# 13
image: postgres:12-alpine
# 14
volumes:
- db_data:/var/lib/postgresql/data/pgd
ata
# 15
environment:
PGDATA: /var/lib/postgresql/data/pgdat
a
POSTGRES_USER: vapor_username
POSTGRES_PASSWORD: vapor_password
POSTGRES_DB: vapor_database
ports:
- '5432:5432'
docker-compose build
docker-compose up -d db
docker-compose up app
Before starting
To perform the steps in this chapter, you must
have an AWS account. If you don’t already have
one, follow the instructions at
https://aws.amazon.com/premiumsupport/know
ledge-center/create-and-activate-aws-account/
to create one.
Value: vapor-til
Host vapor-til
HostName <your public IP or public DNS n
ame>
User ubuntu
IdentityFile </path/to/your/key/file>
ssh vapor-til
wget https://swift.org/builds/swift-5.3.2-re
lease/ubuntu2004/swift-5.3.2-RELEASE/swift-
5.3.2-RELEASE-ubuntu20.04.tar.gz
swift --version
System Memory
The Swift compiler can use a lot of memory.
Small cloud instances, such as a t2.micro, don’t
contain enough memory for the Swift compiler
to work. You can solve this problem by enabling
swap space. In Terminal, enter the following:
sudo su -
fallocate -l 2G /swapfile
chmod 600 /swapfile
mkswap /swapfile
swapon /swapfile
exit
These commands switch to a super user and
create a 2GB swap file. This should be enough to
allow the compiler to work.
# 1
git clone https://github.com/raywenderlich/v
apor-til.git
# 2
cd vapor-til
# 3
swift build -c release --enable-test-discove
ry
./.build/release/Run
sudo su -
apt-get install nginx -y
root /home/ubuntu/vapor-til/Public;
try_files $uri @proxy;
location @proxy {
proxy_pass http://localhost:8080;
proxy_pass_header Server;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_
add_x_forwarded_for;
proxy_connect_timeout 3s;
proxy_read_timeout 10s;
}
}
# 1
rm /etc/nginx/sites-enabled/default
# 2
ln -s /etc/nginx/sites-available/vapor-til \
/etc/nginx/sites-enabled/vapor-til
# 3
systemctl reload nginx
sudo su -
# 2
[Service]
User=ubuntu
EnvironmentFile=/etc/vapor-til.conf
WorkingDirectory=/home/ubuntu/vapor-til
# 3
Restart=always
# 4
ExecStart=/home/ubuntu/vapor-til/.build/rele
ase/Run \
--env production
[Install]
WantedBy=multi-user.target
systemctl daemon-reload
print(req.application.environment) // "produ
ction"
import App
import Vapor
Using Docker
Docker is a great tool for testing and deploying
your Vapor applications. Deployment steps are
coded into a Dockerfile you can commit to
source control alongside your project. You can
execute this Dockerfile to build and run
instances of your app locally for testing or on
your deployment server for production. This has
the advantage of making it easy to test
deployments, create new ones and track
changes to how your deploy your code.
Process monitoring
To run a Vapor application, you simply need to
launch the executable generated by SwiftPM.
Supervisor
Supervisor, also called supervisord, is a popular
process monitor for Linux. This program allows
you to register processes that you would like to
start and stop on demand. If one of those
processes crashes, Supervisor will automatically
restart it for you. It also makes it easy to store
the process’s stdout and stderr in /var/log for
easy access.
supervisorctl reread
supervisorctl update
Systemd
Another alternative that doesn’t require you to
install additional software is called systemd. It’s a
standard part of the Linux versions that Swift
supports. For more on how to configure your
app using systemd, see Chapter 34, “Deploying
with AWS”.
Reverse Proxies
Regardless of where or how you deploy your
Vapor application, it’s usually a good idea to
host it behind a reverse proxy like nginx. nginx
is an extremely fast, battle tested and easy-to-
configure HTTP server and proxy. While Vapor
supports directly serving HTTP requests,
proxying behind nginx can provide increased
performance, security, and ease-of-use. nginx,
for example, can provide support for TLS (SSL),
public file serving and HTTP/2.
Installing Nginx
nginx is usually installed using APT on Ubuntu
but may vary depending on your deployment
method.
apt-get update
apt-get install nginx
## 2
listen 80;
## 3
root /home/vapor/Hello/Public/;
try_files $uri @proxy;
## 4
location @proxy {
## 5
proxy_pass http://127.0.0.1:8080;
## 6
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_
add_x_forwarded_for;
## 7
proxy_connect_timeout 3s;
proxy_read_timeout 10s;
}
}
Logging
Using Swift’s print method for logging is great
during development and can even be a suitable
option for some production use cases. Programs
like Supervisor help aggregate your
application’s print output into files on your
server that you can access as needed.
Horizontal scalability
Finally, one of the most important concerns in
designing a production-ready app is that of
scalability. As your application’s user base grows
and traffic increases, how will you keep up with
demand? What will be your bottlenecks? When
first starting out, a reasonable solution can be to
increase your server’s resources as traffic
increases — adding RAM, better CPU, more disk
space, etc. This is commonly referred to as
scaling vertically.
Load balancing
Now that you understand some of the benefits
of horizontal scaling, you may be wondering
how it actually works. The key to this concept is
load balancers. Load balancers are light-weight,
fast programs that sit in front of your
application’s servers. When a new request
comes in, the load balancer chooses one of your
servers to send the request to.
If one of the servers is unhealthy — responding
slowly or returning errors — the load balancer
can temporarily stop sending requests to that
server.
import Redis
// 1
let redisHostname = Environment
.get("REDIS_HOSTNAME") ?? "localhost"
// 2
let redisConfig =
try RedisConfiguration(hostname: redisHostn
ame)
// 3
app.redis.configuration = redisConfig
app.sessions.use(.redis)
Finally, move
app.middleware.use(app.sessions.middleware) to
below app.sessions.use(.redis). This ensures
that the sessions middleware uses the Redis
sessions configuration.
Supervisor documentation:
http://supervisord.org
nginx documentation:
https://nginx.org/en/docs/
Docker documentation:
https://docs.docker.com
Chapter 36:
Microservices, Part 1
In previous chapters, you’ve built a single Vapor
application to run your server code. For large
applications, the single monolith becomes
difficult to maintain and scale. In this chapter,
you’ll learn how to leverage microservices to
split up your code into different applications.
You’ll learn the benefits and the downsides of
microservices and how to interact with them.
Finally, you’ll learn how authentication and
relationships work in a microservices
architecture.
Microservices
Microservices are a design pattern that’s
become popular in recent years. The aim of
microservices is to provide small, independent
modules that interact with one another. This is
different to a large monolithic application. Such
an approach makes the individual services
easier to develop and test as they are smaller.
Because they’re independent, you can develop
them individually. This removes the need to use
and build all the dependencies for the entire
application.
open Package.swift
URL: http://localhost:8081/users
method: POST
URL: http://localhost:8081/users
method: GET
open Package.swift
URL: http://localhost:8082/
method: POST
short: OMG
long: Oh My God
URL: http://localhost:8082/
method: GET
URL:
http://localhost:8082/user/<ID_OF_THE_US
ER_CREATED_EARLIER>
method: GET
Authentication in Microservices
Currently a user can create, edit and delete
acronyms with no authentication. Like the TIL
app, you should add authentication to
microservices as necessary. For this chapter,
you’ll add authentication to the
TILAppAcronyms microservice. However, you’ll
delegate this authentication to the TILAppUsers
microservice.
import Redis
app.migrations.add(CreateUser())
Authenticating tokens
Now that users can log in and get a token, you
need a way for other microservices to validate
that token and retrieve the user information
associated with it.
method: POST
URL:
http://localhost:8081/auth/authenticate
method: POST
import Vapor
1. Implement respond(to:chainingTo:) as
required by Middleware.
routes.post(use: createHandler)
routes.delete(":acronymID", use: deleteHandl
er)
routes.put(":acronymID", use: updateHandler)
// 1
let data = try req.content.decode(AcronymDat
a.self)
// 2
let user = try req.auth.require(User.self)
// 3
let acronym = Acronym(
short: data.short,
long: data.long,
userID: user.id)
return acronym.save(on: req.db).map { acrony
m }
acronym.userID = user.id
This uses the ID of the request’s authenticated
user. Build and run the app and configure a new
request in RESTed as follows:
URL: http://localhost:8082/
method: POST
short: IKR
docker ps
swift run
This starts the TILAppUsers service. In the
second tab, navigate to the TILAppAcronyms
and run the following command:
swift run
open Package.swift
Forwarding requests
In the TILAppAPI Xcode project, open
UsersController.swift. Below boot(routes:) enter
the following:
// 1
func getAllHandler(_ req: Request)
-> EventLoopFuture<ClientResponse> {
return req.client.get("\(userServiceUR
L)/users")
}
// 2
func getHandler(_ req: Request) throws
-> EventLoopFuture<ClientResponse> {
let id = try req.parameters.require("use
rID", as: UUID.self)
return req.client.get("\(userServiceUR
L)/users/\(id)")
}
// 3
func createHandler(_ req: Request)
-> EventLoopFuture<ClientResponse> {
return req.client.post("\(userServiceUR
L)/users") {
createRequest in
// 4
try createRequest.content.encode(
req.content.decode(CreateUserData.se
lf))
}
}
// 2
func getHandler(_ req: Request) throws
-> EventLoopFuture<ClientResponse> {
let id =
try req.parameters.require("acronymI
D", as: UUID.self)
return req.client.get("\(acronymsService
URL)/\(id)")
}
// 1
acronymsGroup.get(use: getAllHandler)
// 2
acronymsGroup.get(":acronymID", use: getHand
ler)
URL: http://localhost:8080/api/users
method: GET
URL: http://localhost:8080/api/users/login
method: POST
acronymsGroup.post(use: createHandler)
method: POST
short: IRL
// 1
acronymsGroup.put(":acronymID", use: updateH
andler)
// 2
acronymsGroup.delete(":acronymID", use: dele
teHandler)
Handling relationships
In the previous chapter, you saw how
relationships work with microservices. Getting
relationships for different models is difficult for
clients in an microservices architecture. You can
use the API gateway to help simplify this.
Getting a user’s acronyms
In Xcode, open UsersController.swift. Below
loginHandler(_:), add a new route handler to get
a user’s acronyms:
URL:
http://localhost:8080/api/acronyms/<ID_OF
_ACRONYM_YOU_CREATED>/user
method: GET
init(
acronymsServiceHostname: String,
userServiceHostname: String) {
acronymsServiceURL =
"http://\(acronymsServiceHostname):808
2"
userServiceURL = "http://\(userServiceHo
stname):8081"
}
init(
userServiceHostname: String,
acronymsServiceHostname: String) {
userServiceURL = "http://\(userServiceHo
stname):8081"
acronymsServiceURL =
"http://\(acronymsServiceHostname):808
2"
}
// 1
if let users = Environment.get("USERS_HOSTNA
ME") {
usersHostname = users
} else {
usersHostname = "localhost"
}
// 2
if let acronyms = Environment.get("ACRONYMS_
HOSTNAME") {
acronymsHostname = acronyms
} else {
acronymsHostname = "localhost"
}
// 3
try app.register(collection: UsersController
(
userServiceHostname: usersHostname,
acronymsServiceHostname: acronymsHostnam
e))
try app.register(collection: AcronymsControl
ler(
acronymsServiceHostname: acronymsHostname,
userServiceHostname: usersHostname))
init(authHostname: String) {
self.authHostname = authHostname
}
"http://\(authHostname):8081/auth/authentica
te"
# 1
version: '3'
services:
# 2
postgres:
image: "postgres"
environment:
- POSTGRES_DB=vapor_database
- POSTGRES_USER=vapor_username
- POSTGRES_PASSWORD=vapor_password
# 3
mysql:
image: "mysql"
environment:
- MYSQL_USER=vapor_username
- MYSQL_PASSWORD=vapor_password
- MYSQL_DATABASE=vapor_database
- MYSQL_RANDOM_ROOT_PASSWORD=yes
# 4
redis:
image: "redis"
Here’s what’s happening:
Modifying Dockerfiles
Before you can run everything, you must change
the Dockerfiles. Docker Compose starts the
different containers in the requested order but
won’t wait for them to be ready to accept
connections. This causes issues if your Vapor
application tries to connect to a database before
the database is ready. In TILAppAcronyms, open
Dockerfile and replace:
ENTRYPOINT ["./Run"]
CMD ["serve", "--env", "production", "--host
name", "0.0.0.0", "--port", "8080"]
docker-compose up