Pragmatism in the real world

Kitura tutorial part 4: Configuration

Now that we have created a working endpoint in part 3, lets look at the configuration. At the moment, it’s all hard coded! We can do better than that.

If we’re ever going to run this application in the cloud, then it’s advantageous to use environment variables for configuration as per the 12 Factor App guidelines as these vary per deploy.

Environment variable configuration works especially well in containerised / VM applications, but can be a little more problematic when developing on your local computer as different apps may have the same envrionment variable names. To solve this, .env files (pronounced “dotenv”) have become popular for storing configuration, especially in development and test environments.

We will use a .env file locally, but not store it in version control.

.env

# CouchDB configuration
DB_HOSTNAME="localhost"
DB_PORT=5984
DB_HTTPS=false
DB_USERNAME=rob
DB_PASSWORD=123456
DB_NAME=bookshelf_db

# App configuration
APP_PORT=8090

This file contains the 6 confiugration values we need for our CouchDB configuration along with the port we wish to run our API on as we may not always want to use port 8090.

.env files are only to be used in development, so we don’t need it in our git repository. Hence the first thing we do is add it to .gitignore:

.gitignore

.build/*
Packages/*
.env

Adding SwiftDotEnv<

To read our .env file, we are going to use the SwiftDotEnv package. This library will read our .env file and put the variables into the environment for us. It also provides some convenience methods for reading environment variables.

It is used like this:

import DotEnv

let env = DotEnv()

let someString = env.get("SOME_STRING") ?? "example"
let someNumber = env.getInt("SOME_NUMBER") ?? 123
let someFlag = env.getBool("SOME_BOOLEAN") ?? true

As you can see, DotEnv provides three get methods:

  • get() returns a String?
  • getInt() returns an Int?
  • getBool() returns a Bool? where case-insensitive "true", "yes" and "1" evaluate to true

We then can use the nil coalescing operator (??) to unwrap with a default if the environment variable doesn’t exist.

To install it, we add its GitHub URL to the dependencies array in our Package.swift file:

Package.swift

import PackageDescription

let package = Package(
name: "BookshelfAPI",
    dependencies: [
        .Package(url: "https://github.com/IBM-Swift/Kitura.git", majorVersion: 0, minor: 25),
        .Package(url: "https://github.com/IBM-Swift/HeliumLogger.git", majorVersion: 0, minor: 14),
        .Package(url: "https://github.com/IBM-Swift/Kitura-CouchDB.git", majorVersion: 0, minor: 25),
        .Package(url: "https://github.com/SwiftOnTheServer/SwiftDotEnv.git", majorVersion: 0),
    ]
)

A quick run of make will download the library into our Packages directory and compile it.

We can now update main.swift to take advantage of our new configuration system.

Firstly, we import the DotEnv module:

Sources/BookshelfAPI/main.swift

import DotEnv

Now we can update our hardcoded configuration values:

Sources/BookshelfAPI/main.swift<

Replace:

let connectionProperties = ConnectionProperties(
    host: "localhost",
    port: 5984,
    secured: false,
    username: "rob",
    password: "123456"
)
let databaseName = "bookshelf_db"

with:

let env = DotEnv()

let connectionProperties = ConnectionProperties(
    host: env.get("DB_HOST") ?? "localhost",
    port: Int16(env.getAsInt("DB_PORT") ?? 5984),
    secured: env.getAsBool("DB_HTTPS") ?? false,
    username: env.get("DB_USERNAME") ?? "rob",
    password: env.get("DB_PASSWORD") ?? "123456"
)
let databaseName = env.get("DB_NAME") ?? "bookshelf_db"

We have now replaced all our hardcoded values with the value from the environment. If the environment variable doesn’t exist then we provide defaults so that the ?? operator can unwrap the Optional from the get methods.

We also need to update the port that Kitura will serve our API on:

Sources/BookshelfAPI/main.swift

Replace:

Kitura.addHTTPServer(onPort: 8090, with: router)

with:

let port = env.getAsInt("APP_PORT") ?? 8090
Kitura.addHTTPServer(onPort: port, with: router)

All done

With those changes, you can now make the app and run it. Nothing should change. However if you edit .env and change, say APP_PORT to 8091 then when you make and run you should see that it’s now listening on port 8091:

$ ./.build/debug/BookshelfAPI
VERBOSE: init() Router.swift line 57 - Router initialized
INFO: BookshelfAPI main.swift line 41 - Starting server
VERBOSE: run() Kitura.swift line 44 - Staring Kitura framework...
VERBOSE: run() Kitura.swift line 46 - Starting an HTTP Server on port 8091...
INFO: listen(socket:port:) HTTPServer.swift line 132 - Listening on port 8091

Let’s now look at adding some more end points in part 5.

Tutorial navigation

GitHub repository: kitura_bookshelfapi