Experimentation with Vapor and Express

This article is a bit special, because I’m going to present you an experiment on which I worked today.

I wanted to try out the server side development in Swift. So I’ve started by writing a small app in Node.js and then I wrote the same features in Swift. This application is quite simple because it only display a list of items, and allows us to add and remove items from this list.

For this test I used Express as web framework in the Node.js side, and Vapor in the Swift side. I used Redis for the (memory) database and I packaged them in Docker containers in order to make the distribution easier.

Environment

In this article I will assume that you are working on macOS but everything can be achieved on Linux.

In order to run the sample codes you’ll need to install Docker on your machine because we will use docker-compose to orchestrate all our services.

You can download the final code sources on GitHub.

Node.js side

To develop our small app, we are going to use Express as the web framework, Handlebars as template engine and Redis as database. I’m not going to explain you how to install everything because Docker will do that for us.

Let’s get starting by defining the dependencies listed above in the package.json:

{
  "name": "node-server",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "start": "node index.js"
  },
  "author": "Yannick Loriot  (https://yannickloriot.com)",
  "license": "MIT",
  "dependencies": {
    "body-parser": "^1.18.3",
    "express": "^4.16.4",
    "express-handlebars": "^3.0.0",
    "redis": "^2.8.0"
  }
}

Here we have add the body-parser dependency to be able to read the request body send by the HTML form. Now let’s set up the server in the index.js:

const bodyParser = require('body-parser');
const express = require('express');
const exphbs  = require('express-handlebars');
const redis = require("redis");

const port = 3000;

// 1
const app = express();

// 2
app.use(bodyParser.urlencoded({ extended: true }));

// 3
app.engine('handlebars', exphbs({}));
app.set('view engine', 'handlebars');

// 4
const client = redis.createClient({ host: process.env.REDIS_HOSTNAME });
client.on('error', function (err) {
    console.log(`Error ${err}`);
});

// 5
app.listen(port, () => console.log(`Example app listening on port ${port}!`));

In this code we are setting up the components:

  1. creates the Express application
  2. adds the body-parser middleware to the application
  3. sets up the handlebars template engine
  4. creates and connects a Redis client
  5. runs the web server on the given port (here the port 3000)

The template engine is useful if we have a template. So we are going to create our template file in the views/home.handlebars:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>Example App</title>
</head>
<body>
    <h1>Item list</h1>
    <ul>
        {{#items}}
            <li>
                {{.}} - <a href="/delete/{{.}}">Delete</a>
            </li>
        {{/items}}
    </ul>
    <div>
        <form method="POST">
            <label for="title">Title: </label>
            <input type="text" name="title" id="title" required>

            <input type="submit" value="Add">
        </form>
    </div>
</body>
</html>

The HTML (with handlebars tags) code above allows us to render the item list dynamically inside an unordered list. Here a screenshot of the result:

screenshot of the rendering

Now we are going to define the routes to get the home page, post a new item and delete an existant item:

// 1
app.get('/', (req, res) => {
    client.smembers('items', (err, items) => {
        if (err) return res.status(500).send({ error: err });

        res.render('home', { items });
    })
});

// 2
app.post('/', (req, res) => {
    client.sadd('items', req.body.title, err => {
        if (err) return res.status(500).send({ error: err });

        res.redirect('/');
    });
});

// 3
app.get('/delete/:itemId', (req, res) => {
    client.srem('items', req.params.itemId, err => {
        if (err) return res.status(500).send({ error: err });

        res.redirect('/');
    });
});
  1. “GET /”: fetches the items from the redis database and then render the home template passing the items as parameter
  2. POST /“: adds a new item in the items set of redis using the title available in the request body, and then redirects to the home
  3. GET /delete/:itemId“: deletes the given itemId from the items set of redis, and then redirects to the home

To distribute our Node.js app we are going to create its associated Dockerfile:

FROM node:11.9

WORKDIR /usr/src/app

COPY package.json .
COPY package-lock.json .
RUN npm install

RUN mkdir views
COPY index.js .
COPY views/*.* ./views

ENV REDIS_HOSTNAME=redis
ENV REDIS_PORT=6379
EXPOSE 3000

CMD [ "npm", "start" ]

Here we are! We finished our small web app in Node.js and in the next section we are going to do the same thing using Vapor and Swift.

Swift side

For the Swift version of the small app we will use the Vapor web framework because it provides all the components we needs and because this the most popular framework in the Swift world, so it is well maintained and there are a lot of ressources available.

Firstly we are going to define the Package.swift file to manage the dependencies with Swift Package Manager (SPM):

import PackageDescription

let package = Package(
    name: "swift-server",
    dependencies: [
        .package(url: "https://github.com/vapor/multipart.git", from: "3.0.0"),
        .package(url: "https://github.com/vapor/leaf.git", from: "3.0.0"),
        .package(url: "https://github.com/vapor/redis.git", from: "3.0.0"),
        .package(url: "https://github.com/vapor/vapor.git", from: "3.0.0"),
    ],
    targets: [
        .target(name: "Server", dependencies: ["Multipart", "Leaf", "Redis", "Vapor"]),
    ]
)

Like with the Node.js counterpart, we need to parse the request body (with the multipart middleware), a template engine (here leaf) for the rendering and the redis provider.

Now we have all our components we can create our server in the Sources/Server/main.swift:

import Leaf
import Redis
import Vapor

// 1
var databases = DatabasesConfig()
var redisConfig = RedisClientConfig()
redisConfig.hostname = ProcessInfo.processInfo.environment["REDIS_HOSTNAME"] ?? redisConfig.hostname

let redisDatabse = try RedisDatabase(config: redisConfig)
databases.add(database: redisDatabse, as: .redis)

// 2
var services = Services.default()
try services.register(LeafProvider())
try services.register(RedisProvider())
try services.register(databases)

// 3
var config = Config.default()
config.prefer(LeafRenderer.self, for: ViewRenderer.self)

// 4
let app = try Application(config: config, services: services)
let router = try app.make(Router.self)

try app.run()

Here’s what’s happening at each of the numbered comments in the code above:

  1. configures the Redis database connector to access to the redis endpoint
  2. registers the providers (leaf and redis)
  3. configures the leaf template as default view renderer
  4. creates the application with the previously defined configuration and services and runs it

Next we have to create the template view in the Resources/Views/home.leaf:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>Example App</title>
</head>
<body>
    <h1>Item list</h1>
    <ul>
        #for(item in items) {
            <li>
                #(item) - <a href="/delete/#(item)">Delete</a>
            </li>
        }
    </ul>
    <div>
        <form method="POST">
            <label for="title">Title: </label>
            <input type="text" name="title" id="title" required>

            <input type="submit" value="Add">
        </form>
    </div>
</body>
</html>

Then we must declare the routes to serve the template and add / remove items:

// 1
router.get("/") { req -> Future in
    return req.withNewConnection(to: .redis) { redis in
        return redis.smembers("items").flatMap(to: View.self) { data in
            let items = (data.array ?? []).compactMap { $0.string }
            let context = ["items": items]

            return try req.view().render("home", context)
        }
    }
}

// 2
router.get("/delete", String.parameter) { req -> Future in
    let param = try req.parameters.next(String.self)
    let itemId = param.removingPercentEncoding ?? ""

    return req.withNewConnection(to: .redis) { redis in
        return redis.srem("items", items: [RedisData(bulk: itemId)]).map(to: Response.self) { _ in
            return req.redirect(to: "/")
        }
    }
}

// 3
router.post("/") { req -> Future in
    let itemId = try req.content.syncGet(String.self, at: "title")

    return req.withNewConnection(to: .redis) { redis in
        return redis.sadd("items", items: [RedisData(bulk: itemId)]).map(to: Response.self) { _ in
            return req.redirect(to: "/")
        }
    }
}

Here we have created 3 routes which are equivalent to the Node.js ones:

  1. GET /“: fetches the items from the redis database and then render the home template passing the items as parameter
  2. POST /“: adds a new item in the items set of redis using the title available in the request body, and then redirects to the home
  3. GET /delete/:itemId“: deletes the given itemId from the items set of redis, and then redirects to the home

Unlike the javascript version here the code is strictly typed and we use Future instead of callbacks.

To finish we are going to package the application in a container using a Dockerfile:

FROM swift:4.2

RUN apt-get -qq update && apt-get -q -y install \
  tzdata \
  && rm -r /var/lib/apt/lists/*

RUN apt-get -qq update && apt-get install -y \
  libicu55 libxml2 libbsd0 libcurl3 libatomic1 \
  tzdata \
  && rm -r /var/lib/apt/lists/*

WORKDIR /app
COPY . .
RUN mkdir -p /build/lib && cp -R /usr/lib/swift/linux/*.so /build/lib
RUN swift build -c release && mv `swift build -c release --show-bin-path` /build/bin

ENV REDIS_HOSTNAME=redis
ENV REDIS_PORT=6379
EXPOSE 4000
ENTRYPOINT /build/bin/Server serve --hostname 0.0.0.0 --port 4000

We build our container from the official Swift image and we run our application on the port 4000.

App orchestration

To run our 2 applications using the same Redis database we are going to use docker-compose. Here is the docker-compose.yml:

version: '3.7'

services:
  swift-server:
    build: swift
    ports:
      - "4000:4000"
    depends_on:
      - redis
    environment:
      REDIS_HOSTNAME: redis
    restart: always
  node-server:
    build: node
    ports:
      - "3000:3000"
    depends_on:
      - redis
    environment:
      REDIS_HOSTNAME: redis
    restart: always
  redis:
    image: "redis:5"
    restart: always
    ports:
      - "6379:6379"

Here we define the swift-server, the node-server and the redis services. The swift-server and the node-server depends on the redis service. We make the swift-server accessibles to the port 4000 and the node-server to the port 3000.

Now we can build our images by calling the sudo docker-compose build command. Once the build finished we can run them by calling the sudo docker-compose up.

Once our services up, you can access to the node version on the port 3000 and the swift version on the port 4000. As the both services share the same database when you add an item from one of the service, when you refresh the another one you will have the same result.

To go for further

The next iteration should use the websocket in order to refresh the list automatically, and we should also add some authentication to avoid unwanted entries.

I hope this article has helped you in the introduction of the Vapor framework. If you have any question, just let a comment bellow.

1 Star2 Stars3 Stars4 Stars5 Stars (2 votes, average: 5.00 out of 5)
Loading...

0 comments

Time limit is exhausted. Please reload CAPTCHA.

This site uses Akismet to reduce spam. Learn how your comment data is processed.