Pragmatism in the real world

Kitura tutorial part 3: List books

In part two we installed CouchDB and seeded it with some records. The next step is to retrieve a list of books from the database and present it.

We’ll start by looking at how we retrieve the data from the database using the Kitura-CouchDB library. This library enables us to interact with a CouchDB database.

Update Package.json

Firstly, we need to add another dependency to our project in order to interact with CouchDB. The library we will use is called Kitura-CouchDB, so open Package.swift and add this line to the list of dependencies:

.Package(url: "https://github.com/IBM-Swift/Kitura-CouchDB.git", majorVersion: 0, minor: 25),

Using Kitura-CouchDB

To use the Kitura-CouchDB library, we instantiate a CouchDBClient object and then ask it for an instance of a Database which we then use to query the database. This looks like this:

client = CouchDBClient(connectionProperties: connectionProperties)
database = client.database(databaseName)
// do something with database, such as queryByView() or retrieve(), etc.

We’ll represent each book in the database as a Book class instance and put all our database operation code within a mapper class called BooksMapper. Let’s start with a Book:

Sources/BookshelfAPI/Book.swift

import SwiftyJSON

struct Book {
    let id: String
    let title: String
    let author: String
    let isbn: String

    init(id: String, title: String, author: String, isbn: String) {
        self.id = id
        self.title = title
        self.author = author
        self.isbn = isbn
    }

    func toJSON() -> JSON {
        return JSON([
            "title": title,
            "author": author,
            "isbn": isbn,
        ])
    }
}

In Swift, a structure is essentially the same as a class where the key differences are that you can inherit from a class, classes are passed by reference whereas structures are passed by value and classes have a destructor. As a result, we use a struct for our Book instances.

A Book is very simple. It contains 4 constants and an initializer to set them, along with a single method, toJSON() which returns a JSON object that represents this book.

We can now write a database mapper class that will interact with CouchDB which will retrieve a list of books from the database:

Sources/BookshelfAPI/BooksMapper.swift

import CouchDB
import Foundation
import LoggerAPI
import SwiftyJSON

class BooksMapper {
    let database: Database;

    init(withDatabase db: Database) {
        self.database = db
    }

    ///
    /// Fetch all books using the books/all view
    ///
    func fetchAll() -> [Book]? {
        var books: [Book]?

        database.queryByView("all_books", ofDesign: "main_design", usingParameters: []) {
            (document: JSON?, error: NSError?) in
            if let document = document {
                // create an array of Books from document
                if let list = document["rows"].array {
                    books = list.map {
                        let data = $0["value"];
                        let bookId = data["_id"].stringValue + ":" + data["_rev"].stringValue

                        return Book(id: bookId, title: data["title"].stringValue,
                            author: data["author"].stringValue, isbn: data["isbn"].stringValue)
                    }
                }
            } else {
                Log.error("Something went wrong; could not fetch all books.")
                if let error = error {
                    Log.error("CouchDB error: \(error.localizedDescription). Code: \(error.code)")
                }
            }
        }

        return books
    }
}

Let’s break this down and see what’s going on. Firstly the initialiser:

    init(withDatabase db: Database) {
        database = db
    }

In order to use our mapper, we need a database instance, so our initialiser accepts one and stores it into a local property. We take advantage of the ability to have separate internal and external parameter names so that the call to the initialiser reads better.

We then have the fetchAll() method. This method will return an array of Book instances or nil. We use the database instance’s queryByView method to do the work.

    database.queryByView("all_books", ofDesign: "main_design", usingParameters: []) {

The queryByView method takes the name of the view and which design it part of as it’s first two parameters (“all_books” in “main_design” in this case). The third parameter (usingParameters) is used to pass any query parameters to the view: none in this case. The final parameter to the method is a closure which is called when the method finishes.

The signature of the closure is:

func callback(document: JSON?, error: NSError?)

This means that we’ll either get back a JSON document or an error. Both are marked optional as in the success case, error is nil and on failure, document is nil. This requires a certain amount of unpacking.

Firstly we deal with the success case:

        if let document = document {
            // create an array of Books from document
            if let list = document["rows"].array {
                books = list.map {
                    let data = $0["value"];
                    let bookId = data["_id"].stringValue + ":" + data["_rev"].stringValue

                    return Book(id: bookId, title: data["title"].stringValue,
                        author: data["author"].stringValue, isbn: data["isbn"].stringValue)
                }
            }
        }

We unwrap the optional document first. On success, CouchDB will return our data within the “rows” array within the JSON. This is mapped to an optional in SwiftyJSON, so we unwrap it to the constant list. We can then iterate over list using map() to turn each record into a Book instance. Each document returned by CouchDB has three properties: “id”, “key” and “value”. Our data is within the “value” property so we assign that to a local constant, data to make the code easier to read.

The result of the map call is an array of Book instances, so we assign this to the books variable that we defined at the top of the method. This variable is in scope by reference within the closure so we use it to extract the list of books from the closure in order to return them from the fetchAll() method.

If the query fails, ‘document’ is nil, so we handle this in the else section:

        } else {
            Log.error("Something went wrong; could not fetch all books.")
            if let error = error {
                Log.error("CouchDB error: \(error.localizedDescription). Code: \(error.code)")
            }
        }
    }

In this case we simply write some log output. Again, we have to unwrap the error parameter as it’s an optional.

We now have a mechanism to retrieve data from the database, so lets write a Kitura route to produce a list of books.

Adding a route handler for /books

The first route we added to our application was directly within main.swift. However adding every route this way will lead to a rather large file, so lets separate our our new routes into separate handler functions with the same signature as the closure we used for the "/" route.

We’ll start with the /books endpoint. This responds to GET and returns a list of all books in the database.

As SwiftPM doesn’t mind if we create sub directories within our BookshelfAPI directory, we’ll create a Handlers directory to store our handlers in order to provide a little organisation of our source code. The handler for /books is listBooksHandler:

Sources/BookshelfAPI/Handlers/ListBooksHandler.swift:

import CouchDB
import Kitura
import LoggerAPI
import SwiftyJSON

func listBooksHandler(request: RouterRequest, response: RouterResponse, next: ()->Void) -> Void {
    Log.info("Handling /books")
    if let books = booksMapper.fetchAll() {
        var json = JSON([:])

        json["books"] = JSON(books.map { $0.toJSON() })
        json["count"].int = books.count

        response.status(.OK).send(json: json)
    } else {
        response.status(.internalServerError).send(json: JSON(["error": "Could not service request"]))
    }
    next()
}

Let’s look at what this code is doing.

func listBooksHandler(request: RouterRequest, response: RouterResponse, next: ()->Void) -> Void {
    Log.info("Handling /books")

Our function has to have the correct signature that matches the callback signature. This signature is type aliased as the RouterHandler type and matches the one we used earlier for the “/” route. As before, we need to fill in the response before calling next(). It’s always good to add some logging, so we add an info message that this handler has been called.

    if let books = booksMapper.fetchAll() {
        var json = JSON([:])
        json["books"] = JSON(books.map { $0.toJSON()  })
        json["count"].int = books.count

        response.status(.OK).send(json: json)
    }

The fetchAll() method returns the list of books as an Optional, so we unwrap it first. This is the successful path, so we then create a JSON object containing the list of books under the “books” property and the number of books in the “count” property. Note that the books array is a list of Book objects. The Book class knows how to provide a JSON representation of itself via the toJSON() method, so we iterate over the list (via map) to create the JSON data to be sent. We can now set the status of the response to .OK (200) and attach the JSON data to its body.

    } else {
        response.status(.internalServerError).send(json: JSON(["error": "Could not service request"]))
    }

If we couldn’t unwrap the data from fetchAll(), then we failed to retrieve the data. There’s no useful information we can give to the client about the failure, so we set the status to .internalServerError (500) with a generic error in the response’s body.

    next()

Finally we pass control to the next handler in the middleware pipeline. The last handler will return the response to the client.

Connecting our handler to the router

The final part of this puzzle is to connect the listBooksHandler function to the router instance in main.swift.

Firstly, however, we need to set up the dependencies so that we have a booksMapper instance available in the global scope so that listBooksHandler can use it. This is done in main.swift just after the Log.logger = HeliumLogger() statement:

Sources/BookshelfAPI/main.swift

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

let client = CouchDBClient(connectionProperties: connectionProperties)
let database = client.database(databaseName)
let booksMapper = BooksMapper(withDatabase: database)

Let’s break this down.

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

Firstly set up our configuration information for the CouchDB connection in the ConnectionProperties instance along with a constant defining our database name(bookshelf_db).

let client = CouchDBClient(connectionProperties: connectionProperties)
let database = client.database(databaseName
let booksMapper = BooksMapper(withDatabase: database)

This is the chain of dependencies required for our handler. We instantiate an instance of CouchDBClient (client) and use this to retrieve the Database instance (database). We then instantiate a BooksMapper instance (booksMapper) using our database object.

We also need to import the CouchDB module, so add this to the very top of the main.swift:

Sources/BookshelfAPI/main.swift

import CouchDB

Finally, we can add the /books route to the router. This is done just after the router.get("/") statement:

Sources/BookshelfAPI/main.swift

router.get("/books", handler: listBooksHandler)

We’ve now written all the code to implement the /books endpoint which will return a list of books.

Run make to compile it all up and then start the application with

$ .build/debug/BookshelfAPI

Let’s test it!

Testing /books

We return to curl in order to test that it works

$ curl -i http://localhost:8090/books

This should provide an output that starts:

HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 3241
Date: Sun, 31 Jul 2016 15:52:39 GMT
Connection: Close

{
  "count" : 27,
  "books" : [
    {
      "author" : "Anne McCaffrey",
      "title" : "Dragondrums",
      "isbn" : "9780689306853"
    },
    {
      "author" : "Anne McCaffrey",
      "title" : "Dragonflight",
      "isbn" : "9780345335463"
    },

    [etc..]
}

(note that the order of elements in a Swift dictionary is non-deterministic, so you may see elements in a different order.)

Our first endpoint now works! Let’s move on to part 4.

Tutorial navigation

GitHub repository: kitura_bookshelfapi