Creating and deploying a Vapor (Swift) web app into Heroku cloud platform — Part (2/2)

Ricardo Santos
10 min readJan 7, 2021

--

2018 @ https://tinyurl.com/yycevcr3

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)

Image 1

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 your Content 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).

Image 2

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”.

Image 3

Starting our server (locally for now) and using RESTed app for a quick test (image 4)…

Image 4

We have the expect answer from our server depending on the POST credentials!

https://www.farmersalmanac.com/wp-content/uploads/2019/06/Why-Fireworks-History-A209552188.jpg

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.

Image 6

More useful articles about routing

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.

Image 7

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:

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.

Image 8

While working locally, your database environment variables should be declared on your target scheme (image 9).

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)

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…

Image 11

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 Modelprotocol 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……

Image 12

…now that GreetingModelconforms 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 GET
  • greetings/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 }
}
Image 13

Testing GET greetings

Back to RESTed app, we did a GET on the route greetings and we received all stored greetings!

Image 14
https://images.dailyhive.com/20190628065533/montreal-fireworks-canada-day.jpg

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()`

Image 15

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()
}
Image 16

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

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)

--

--