David.dev

Server-side Swift: Kitura and MongoDB tutorial Part 1


In the past weeks I had the chance to work on Server-side swift. The concept is very interesting: use Swift not just for an iOS or Mac OS app but for a server-side web application or backend. 

What? Why would you need this when you can code your shiny API in javascript/NodeJS, PHP, Go, Python etc. ? Well, because you can. And because Apple released Swift as open source in 2014. 

You can run it on linux with two simple commands: swift build and.. you guessed already swift run.

I will now save you tens of hours of research to find out that there are only two major frameworks suitable for web development: Vapor and Kitura.

Kitura was developed and is currently maintained by corporate giant IBM.

Vapor is a an independent effort supported by many developers. Whilst Kitura is somehow conservative on upgrades, Vapor is not afraid of breaking backward compatibility between major versions. 

Kitura borrows a bit more from Express whilst Vapor aims to be more "swift". Another major difference I think is that Vapor is more opinionated: for Authentication it seems to make it impossible to work without fluent, their DB abstraction, and I am not a big fan of this approach. 

The discord channel of Vapor is very busy so the community is very strong. 

Kitura community is more sleepy somehow and seems to need some ❤ī¸ too.  

Both projects are fairly documented but there are very few good tutorials for real life web projects like a CMS (everything seems to be an API nowadays, seriously?).

To get started with Kitura feel free to check out the hello world tutorial.

Prerequisites 

I assume that you have Swift installed, have the hello world working fine and mongo db running. If you are on Mac OS X I suggest to use Xcode for your development.

You will also need the Mongo C driver in Mac OS X just install with brew install mongo-c-driver for linux follow the instructions here 

In this tutorial we are building a small CMS. We will be able to store pages in MongoDB, read them, add new pages, edit pages and update pages.  

Step 1 -  Dependencies 

open your Package.swift and define your dependencies like this:

import PackageDescription
let package = Package(
name: "yourAppName",
dependencies: [
.package(url: "https://github.com/IBM-Swift/Kitura", from: "2.8.0"),
.package(url: "https://github.com/mongodb/mongo-swift-driver", from: "0.2.0"),
.package(url: "https://github.com/IBM-Swift/HeliumLogger.git", from: "1.9.0"),
.package(url: "https://github.com/IBM-Swift/Kitura-StencilTemplateEngine.git", from: "1.11.1"),
.package(url: "https://github.com/IBM-Swift/Kitura-CredentialsHTTP", from: "2.1.3"),
],
targets: [
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
// Targets can depend on other targets in this package, and on products in packages which this package depends on.
.target(
name: "YourAppName",
dependencies: ["Kitura", "MongoSwift","HeliumLogger","KituraStencil","CredentialsHTTP"]),
.testTarget(
name: "tennispressTests",
dependencies: ["tennispress"]),
]
)

So here we are adding Kitura (the framework you need to use swift in your web project) MongoDB (the official swift driver 0.2.0 that is thread safe), Helium (logger), Stencil (template engine to render your pages) and Kitura-CredentialsHTTP is needed to protected the admin section of your site. 

Step 2 Preparation 

Now move to main.swift  (inside Sources-->yourAppName)

and import the packages you need 

import Kitura
import MongoSwift
import HeliumLogger
import LoggerAPI
import KituraStencil
HeliumLogger.use() // here you initialise the logger for the console

// here we define a page of our mini CMS 
 struct Pages: Codable {
var title: String
var slug: String
var body: String
var date: String
}

/// A single collection with type `Pages`. This allows us to directly retrieve instances of

/// `db` is safe to share across threads.

let db = try MongoClient().db("home")

var collection = db.collection("pages", withType: Pages.self)

so our Page struct has a title, a slug (useful for pretty urls) body and the date. The _id is automatically generated by MongoDB. We could also use our slug as _id (in this way we would be sure that it would be a unique value). 

Now we also initialise the constant collection that is our MongoDB connection to a db with the name "home" and a collection called pages. Note that the type has to be specified and this is why we defined it before.  You can now use this connection throughout your routes. 

Step 2 Routes: All Pages  

A small refresher of a Kitura basic hello world route 

import Kitura
let router = Router()
router.get("/") { request, response, next in
response.send("Hello world!")
next()
}
Kitura.addHTTPServer(onPort: 8080, with: router)
Kitura.run()

This is pretty much self explanatory but please pause for a second and reflect on this: how comes that all the web frameworks sell you the idea on how easy is to make an hello world route -- and wow does it feel good when it works -- but often omit the more difficult stuff ? ;) Kitura borrows the express syntax that is pretty straightforward. I like the idea to adopt something many developers might be already familiar with.

Back on topic with our routes: we will first create a route to display all the pages in our CMS:


router.get("/pages") { request, response, next in
let documents = try collection.find()
var pages:[[String:String]] = []
for d in documents {
print(d)
pages.append(["title" : d.title, "slug" : d.slug, "body": d.body, "date": d.date
])
print(d)
}
// check if an error occurred while iterating the cursor
if let error = documents.error {
throw error
}
try response.render("mongopages.stencil", with: ["Pages": pages])
response.status(.OK)
}

A lot going on here: first we define the route in this case /pages will display all pages. Then we define the constant documents that runs the query from our previously defined connection "find()" which means find all documents in a collection.

However, documents doesn't return an array or an object but a cursor. So you need first to define var pages:[[String:String]] = [] to then append our values form the loop.

We then render the pages for the stencil template. Our mongopages.stencil will look like this:

{% extends "base.stencil" %}
{% block content %}
<h1>All Pages </h1>
<table class="mui-table">
<thead>
<tr>
<th>title </th>
<th>slug  </th>
<th>body  </th>
<th>date</th>
</tr>
</thead>
<tbody>
{% for p in Pages %}
<tr>
<td>
{{ p.title }}
</td>
<td>
{{ p.slug }}
<td>
{{ p.body }}
<td>
{{ p.date}}
</td>
{% endfor %}
</tbody>
</table>
{% endblock %}

so to avoid repeating all the basic html we extend our base template and add a block content. 


Step 3 Routes: Single Page

Obviously in a CMS we don't just want a list of pages (we might not even need one in fact) but -- central to the CMS purpose -- is to display pages dynamically. So let's write a dynamic route that will check the db for a page with a defined slug and return it:


router.get("/:name") { request, response, next
in guard let name = request.parameters["name"]
else {
_ = response.send(status: .badRequest)
return next()
}
let query: Document = ["slug": .string(name)]
let documents = try collection.find(query)
// check if an error occurred while iterating the cursor
if let error = documents.error { throw error
}
print("documents")
print(documents)
var page: [String: String] = [:]
for d in documents {
print(d)
page = [
"title": d.title,
"slug": d.slug,
"body": d.body,
"date": d.date,
]
break
}
if page.isEmpty {
// response.send ("<h1>&#127934 not found </h1>")
let title = ["title": "Page not Found - 404 "]
try response.render("404.stencil", context: title)
next()
} else {
try response.render("page.stencil", with: page)
response.status(.OK)
}
}

Few things are different here: first we insert a parameter :name after the main route so /whateveryoutypehere will be inside the constant name.

Now in MongoDB we pass collection.find with a specific query: to look for the document with the slug as typed by the user.  The var page format has also chanted to reflect the different format to be passed to stencil to display a single page. We also break the loop after returning one record. This is a bit of a gimmick as I didn't see findOne being implemented and I found no way to transform the cursor documents without a loop. 

in your stencil template you now need to just add the variables like this

<p> {{ title }}</p>
<p> {{ slug }}</p>
<p> {{ body }}</p>
<p> {{ date }}</p>

now if you visit localhost:8080/index it will return you the page with the slug "index" or any page with a certain slug present in the database. 


Our CMS is now functional: it shows dynamic pages under / and a list of all pages under /pages

In our Part 2 tutorial we will cover insert pages, edit pages, and delete pages.

A big thank you to the MongoDB staff for releasing version 0.2.0 of the driver in record time. This new version "... is safe to share across threads / use for concurrent requests, and in fact we encourage sharing them across threads as of that release."


11

made with ❤ī¸ by david.dev 2024 RSS Feed