Creating and deploying a Vapor (Swift) web app into Heroku cloud platform — Part (2/2)
Intro
This is the second of a serie of 2 articles. On Article 1, we learned to deploy on Heroku cloud platform a web app built using Vapor.
On this article we will improve our web app to deal with more complex HTTP methods (GETs with parameters and POSTs) and connect it with a PostgreSQL database.
If you’re not familiar with Heroku and Vapor,
Heroku is a cloud platform as a service (PaaS) supporting several programming languages, and Vapor is an open source web framework written in Swift that can be used to create RESTful APIs, web apps, and real-time applications using WebSockets.
I — Adding routes
Routing is the process of finding the appropriate request handler for an incoming request.
The routes.swift file is were the routes are declared and is entrance point for our client (apps) requests in our web app.
Intro to GET HTTP Methods
We if wanted to define a route, GET, that returned an hello message, then on the routes.swift
we would declare inside the routes(_ app: Application)
function the following:
func routes(_ app: Application) throws
// YOUR_SERVER_PATH/hello
app.get("hello") { req -> String in
return "Hi!"
}
}
Having a look on the code we see that:
- Its a GET method :
app.get
. - Returns a string :
req -> String
. - The return result is “Hi” .
- The path is
hello
.
What if we wanted to say hello using someone’s name? The solution bellow is a possible away. Just create a new route on the path hello/joe
and so on…
// YOUR_SERVER_PATH/hello/joe
app.get("hello", "joe") { req -> String in
return "Hi Joe!"
}
But maybe this is not the best solution. Lets introduce dynamic routing!
// Routing a request based on the GET HTTP method and DYNAMIC path app.get("hello", ":someone") { req -> String in
guard let name = req.parameters.get("someone") else { throw Abort(.internalServerError) }
return "Hello \(name)."
}
By using a path component prefixed with :
, we indicate to the router that this is a dynamic component. Any string supplied here will now match this route. We can then use req.parameters
to access the value of the string. (source)
Intro to POST HTTP Methods
Before we implement our first POST:
- We must be aware that Vapor projects have conventioned folder structure as the documentation states here.
- The
Models
folder is the place to store yourContent
structs. (Content API) - Content API allows you to easily encode / decode Codable structs to / from HTTP messages.
Now are ready to do a simple authentication route handler.
The first step to decode our HTTP message is to create a codable type that matches the HTTP message body. On this example we will create a structAutenticationModel
(see Image 2) that conforms toContent
(conforming the type to Content
will automatically add conformance to Codable
alongside additional utilities for working with the content API). The structAutenticationModel
will also contain 2 strings, user and password, and should be created inside the Models
folder (Vapor convention).
Also, we will create a route named login
that will handle a POST request (see image 3). That request should contain a JSON-encoded parameters, and those parameters must match our AutenticationModel
struct. The login logic will be very simple: the user name must be “ricardo” and the password must be “1”.
Starting our server (locally for now) and using RESTed app for a quick test (image 4)…
We have the expect answer from our server depending on the POST credentials!
We wont go deeper about routing because its not the point of the article (we still have a database to connect).
Friendly note: We can actually use breakpoints on Xcode to help us debug while doing local requests.
More useful articles about routing
- Vapor 4 routing (hight recommended)
- Vapor Content (hight recommended)
- The complete guide to routing with Vapor 3
- Beginner’s guide to Server side Swift using Vapor 4
II — Preparing your database
About Fluent and PostgreSQL driver:
Fluent is an ORM framework for Swift. It takes advantage of Swift’s strong type system to provide an easy-to-use interface for your database. Using Fluent centers around the creation of model types which represent data structures in your database. These models are then used to perform create, read, update, and delete operations instead of writing raw queries.
Saying so, Fluent is a framework that help us to deal with several database types like PostgreSQL, SQLite, MySQL and others. On this article we will focus on a PostgreSQL database, and if you read already the previous article, you should already have the Fluent and the PostgreSQL driver installed.
If not, just add the Fluent dependency and the PostgreSQL driver dependency on your Package.swift file (image 7).
More details about Fluent can be found here. (hight recommended reading)
Creating a PostgreSQL remote database
For this step you’ll need:
- To own a PostgreSQL database. You can find several online services that can provide you one. On this article we used a PostgreSQL database served by https://www.elephantsql.com (free).
- You will also need a PostgreSQL Client for the Mac. If don’t have a PostgreSQL one, you can use Postico (free).
Using your PostgreSQL Client for the Mac, will log on our database and create a simple table greeting
with a key named id
, and a field named from
(see script bellow).
CREATE TABLE greeting (
id SERIAL PRIMARY KEY,
from text
);
This will be our database table, and our goal is create 2 routes:
- Route 1 : GET all the greetings (from the database).
- Route 1 : POST (add) a new greeting on the database.
Setting up database environment vars
If you look at the Configure.swift file (image 8) on your project (more info about Configure.swift can be found here), you will find the Environment.get("key")
command. We will use it to get our keys from the process environment and avoid to have them hardcoded on the source-code.
While working locally, your database environment variables should be declared on your target scheme (image 9).
You’ll need also to set the environment variables remotely (on Heroku). Go to your app Settings/Config Vars section. These keys will be used when our app is deployed… (image 10)
Friendly note: On image 10, we can find the default way to connect with our database. In my opinion a more elegant and simple way is to use a single connection string. The connection string shape is as follows: postgres://USER:PASSWORD@SERVER:PORT/DBNAME
do {
let dbConnection: String = Environment.get("CONNECTION_STRING")
try app.databases.use(.postgres(url: dbConnection), as: .psql)} catch {
app.logger.error(Logger.Message(stringLiteral:"\(error)")
}
III — Using the database
Final goal
All comes down to this final step!
- We know how to create routes.
- We have a database ready to be used.
Our final goal is simple:
- Add a route that will store greetings (POST) on the database.
- Add a route that can retrieve all greetings (GET) from the database.
To achieve our goal we just need about 25 lines of code (image 11). Not much code, but lots of things are happening here. Lets try to break things down…
GreetingModel
public final class GreetingModel: Model, Content {
public static let schema = "greeting"
@ID(custom: "id")
public var id: Int? @Field(key: "from")
var greetingFrom: String public init() {
self.greetingFrom = ""
} public init(greetingFrom: String = "") {
self.greetingFrom = greetingFrom
}
}
We have a struct named GreetingModel
. This struct will be a wrapper around our table records.
Notice that it conforms with Content
and Model
.
AboutContent
protocol compliance, will automatically add conformance to Codable
alongside additional utilities for working with the content API.
About Model
protocol compliance, GreetingModel
will need to implement schema
(our table name) and an $ID(custom:)
property wrapper matching the table primary key. We also need the $Field(key:)
property wrappers to bind with all the table columns. BUT……
…now that GreetingModel
conforms with Model
, GreetingModel
will automatically earn the implementation of database utils like save
, create
, update
, query
and others given by the Fluent framework.
This will allow us to do things like GreetingModel.query(on: app.db).all()
.
Routing
Now that we have GreetingModel
that can wrapper the connection with the table greetings
(on our database), is time for routing.
We added two methods on routes(_ app: Application)
function
greetings
GETgreetings/add
POST
app.get("greetings") { req -> EventLoopFuture<[GreetingModel]> in
let db = Bool.random() ? req.db : app.db
return GreetingModel.query(on: db).all()
}app.post("greetings", "add") { req -> EventLoopFuture<GreetingModel> in
let record = try req.content.decode(GreetingModel.self)
let db = Bool.random() ? req.db : app.db
return record.create(on: db).map { record }
}
We havelet db = Bool.random() ? req.qd : app.db
. This is just to show that we can acquire our database reference using the app
parameter passed on routes(_ app: Application)
, but also using the req
(request) variable.
About the route greetings
, it uses GreetingModel.query(on: app.db).all()
that we already are familiar with and returns a GreetingModel
array.
About the route greetings/add
, receives a JSON on POST request body, that will decode using req.content.decode(GreetingModel.self)
and store on the database using record.create(on: bd)
and finally take the saved object (now with property public var id: Int?
with the key and return everything on the response body .map { record }
.
Testing POST greetings/add
Back to RESTed app, we did a POST on the route greetings/add with a greeting on the body (notice that we are not sending the greeting id) and if your look back at the code (below) we are saving our greeting and returning the greeting back (now with the id filled).
app.post("greetings", "add") { req -> EventLoopFuture<GreetingModel> in
let record = try req.content.decode(GreetingModel.self)
let db = Bool.random() ? req.db : app.db
return record.create(on: db).map { record }
}
Testing GET greetings
Back to RESTed app, we did a GET on the route greetings and we received all stored greetings!
IV — Extra notes
wait() and EventLoop
TLDR:.wait()
is super handy for debug, but we cant use it on routing.
We know that GreetingModel.query(on: bd).all()
will return a EventLoopFuture
and EventLoopFuture
principle is:
all callbacks registered on an `EventLoopFuture` will execute on the thread corresponding to the event loop that created the `Future`. If the `EventLoopFuture` resolves with a value, that value is returned from `wait()`
Now notice the diference between GreetingModel.query(on: bd).all()
that we used on func routes(_ app: Application)
and GreetingModel.query(on: bd).all().wait()
used onconfigure(_ app: Application)
(for debug purposes).
func configure(_ app: Application) throws {
...
if let results = try? GreetingModel.query(on: app.db).all()
.wait() {
print("--------------------")
print(results as Any)
}
vs
func routes(_ app: Application) {
...
app.get("greetings") { req -> EventLoopFuture<[GreetingModel]> in
let db = Bool.random() ? req.db : app.db
return GreetingModel.query(on: db).all()
}
}
While starting the application, we where on the main tread, so there was no “problem” blocking it for a while to check if everything was OK with our database. This way, there was no problem using the .wait()
command. How ever, if the try do to same thing inside routers(_ app: Application)
:
app.get("greetings_crash") { req -> EventLoopFuture<[GreetingModel]> in
try? GreetingModel.query(on: app.db).all().wait()
return GreetingModel.query(on: req.db).all()
}
We will crash (image 16), and this is why:
`wait()` will block whatever thread it is called on, so it must not be called on event loop threads: it is primarily useful for testing, or for building interfaces between blocking and non-blocking code.
References
- Vapor
- Vapor : Query
- Vapor : Fluent
- Vapor : Folder structure
- Vapor : Content API
- Vapor : Routing
- Vapor file : Configure.swift
- Heroku Logs : The Complete Guide
- RESTed : Simple HTTP Requests Client for the Mac
- Postico : PostgreSQL Client for the Mac
- PostgreSQL as a Service
V — Materials
All materials for this article can be found at here.
Here is current state of my web app (it’s more advanced than what you can find on this article)