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
- Part 1: Getting Started with Kitura
- Part 2: CouchDB
- Part 3: List books
- Part 4: Configuration
- Part 5: Hypermedia
- Part 6: Adding a book
GitHub repository: kitura_bookshelfapi