Swift Web Frameworks

Episode #216 | 19 minutes | published on April 7, 2016 | Uses Frank-0.2, swift-dev-2016-03-16a, vagrant-1.8.5
Subscribers Only
Running Swift on Linux is intriguing because it allows us to create web applications with Swift and host them on an inexpensive Linux VPS. In this episode we'll use Vagrant to create an Ubuntu virtual machine, install a working version of the Swift development snapshot, and write a tiny web application, complete with routing, parameter extraction, template rendering with Stencil, and JSON parsing. You'll learn about Swift Build, specifying version dependencies, and where to look for the source code.

Episode Links

  • Source Code
  • NSDateFormatter.com - a side project I launched that runs Swift on Linux and the libraries covered in this video
  • Frank - Frank is a DSL for quickly writing web applications in Swift
  • Currasow - Swift HTTP server using the pre-fork worker model
  • Stencil - Stencil is a simple and powerful template language for Swift

Setting up the Virtual Machine

To setup our Vagrant machine, we can define the Vagrantfile like this:


Vagrant.configure(2) do |config|
  config.vm.box = "ubuntu/trusty64"
  config.vm.network "forwarded_port", guest: 8000, host: 8082
  config.ssh.pty = true
  config.vm.synced_folder ".", "/srv/hello-world"
  config.vm.provision "shell", privileged: false, inline: <<-SHELL
    sudo apt-get update
    sudo apt-get install -y git clang libicu-dev nginx
    if [[ ! -f swift-DEVELOPMENT-SNAPSHOT-2016-03-01-a-ubuntu14.04.tar.gz ]]
    then
      wget https://swift.org/builds/development/ubuntu1404/swift-DEVELOPMENT-SNAPSHOT-2016-03-01-a/swift-DEVELOPMENT-SNAPSHOT-2016-03-01-a-ubuntu14.04.tar.gz
      tar xzvf swift-DEVELOPMENT-SNAPSHOT-2016-03-01-a-ubuntu14.04.tar.gz
    fi
    echo "export PATH=/home/vagrant/swift-DEVELOPMENT-SNAPSHOT-2016-03-01-a-ubuntu14.04/usr/bin:$PATH" >> ~/.profile
    source ~/.profile
    swift --version
    sudo chown vagrant:vagrant /srv/hello-world
  SHELL
end

Defining our Dependencies

In the Package.swift file, specify our package name & dependencies:

import PackageDescription

let package = Package(
  name: "HelloWorld",
  dependencies: [
  .Package(url: "https://github.com/nestproject/Frank.git", majorVersion: 0, minor: 2),
  .Package(url: "https://github.com/kylef/Stencil", majorVersion: 0),
  .Package(url: "https://github.com/czechboy0/Jay.git", Version(0, 3, 5))
])

Note that we're locking our version of Jay to 0.3.5 so we don't get their latest changes for Swift 3 compatibility, because it won't compile with our chosen dev snapshot of Swift.

Writing a Simple Endpoint with Frank

Open up main.swift and define your first endpoint:

import Frank

get { request in
  return "Hello World"
}

We can then build & run our project:

$ swift build
...

$ .build/debug/HelloWorld

This will launch the project and start hosting the web application. Hit the URL from your host machine (using the port you specified in the Vagrantfile. For me, this was http://localhost:8082 on my Mac.

Define Dynamic Templates with Stencil

To have dynamic HTML returned, we'll use Stencil. To render a template, we'll first define a helper method that will compile & render the template using some provided variables.

Create stencil.swift and type the following:

import Stencil
import Inquiline
import Frank
import PathKit

func stencil(path: String, _ context: [String: Any] = [:]) -> ResponseConvertible {
  do {
    let template = try Template(path: Path("Resources") + Path(path))
    let body = try template.render(Context(dictionary: context))
    return Response(.Ok, headers: [("Content-Type", "text/html")], body: body)
  } catch {
    print("error reading template: \(path)")
    return Response(.InternalServerError)
  }
}

Here we try to load a template from he Resources directory (relative to our project's root). If it is not found (or if we have an error in the template itself, we'll get an error. We must catch this error and render a proper HTTP response (500 in this case). If we don't, a simple error will crash the web server!

With this in place, we can go back to main.swift and add a new endpoint:

get("users", *) { (request, username: String) in
  stencil("index.stencil", ["username": username])
}

Here we take an argument in the URL and pass that off to our template. The template looks like:

<h1>Hello {{username}}</h1>

If we build & run the application now we can see our changes. Open up http://localhost:8082/users/ben on your host machine and see the result.

Rendering JSON

To create an API we'll usually be returning JSON data. Define a new helper in render_json.swift:

import Frank
import Foundation
import Inquiline
import Jay

func renderJSON(object: Any, status: Status = .Ok) -> ResponseConvertible {
  do {
    let headers = [("Content-Type", "application/json")]

    let jsonData = try Jay().dataFromJson(object)
    let json = try jsonData.string()

    return Response(status, headers: headers, body: json)
  } catch {
    print("Error serializing \(object) to JSON")
    return Response(.InternalServerError)
  }
}

We can then use this in our endpoint definition like so:

import Foundation

get("api") { request in
  return renderJSON(["date": "\(NSDate())"])
}

The if we build the project, restart the server, and visit /api in the browser, we'll see today's date returned as a JSON response.

blog comments powered by Disqus