Xcoding with Alfian

Software Development Videos & Tutorials

Server Side Swift CRUD API with Kitura Codable Routing

Alt text

Kitura is a Server Side Swift web framework created by IBM. It’s licensed under MIT License, free to use, and the source code is open source. Recently with the 2.0 release, Kitura team introduces the new Codable Routes feature which make it very easy for the developers to handle URL Request and Response automatically using Swift 4 Codable.

Swift 4 introduces Codable protocol, which means a type that implements Codable protocol has the ability to decode and encode its properties to any external representation such as JSON. Many primitive data type such as Int, String, Double, Data, URL already is a Codable type at language level, Array and Dictionary that contains a Codable type also is already a Codable type.

struct TodoItem: Codable {
    var id: String
    var title: String
    var content: String
}

// Encoding TodoItem struct to JSON Data
let todoItem = TodoItem(id: "1", title: "hello", content:"world")
let jsonData = try! JSONEncoder.encode(todoItem)// Decoding JSON Data to TodoItem struct
let todoItem2 = try! JSONDecoder.decode(TodoItem.self, from: jsonData)

Encoding and Decoding a Codable object from JSON and vice versa is very easy with Swift built in JSONEncoder and JSONDecoder class. With Codable, we don’t have to parse the JSON manually or using third party library such as SwiftyJSON. Codable does the job like magic.

Kitura Codable Routes

With Kitura Codable Routes, we can create a Router endpoint method that accepts a Codable object for the request and a completion closure that will be passed the Codable object as the response. As a developer, we don’t need to write the encode and decode logic manually anymore. To see all of this in actions, let’s build a CRUD API for TodoItem with in memory datastore.

import Kitura
import KituraContracts

struct TodoItem: Codable {
   var id: String
   var title: String
   var detail: String
}
var todos = [
    TodoItem(id: "1", title: "Clean the house", detail: "My house"),
    TodoItem(id: "2", title: "Wash your car", detail: "Your personal car")]
let router = Router()

Retrieve List of TodoItem

We can retrieve list of TodoItem by using the GET method for the Router and specify /todos as the path. Then we pass a closure to the handler parameter that itself has completion closure for optional Array of TodoItem and RequestError. In this simple case, we just invoke the completion passing our in memory todos TodoItem Array. We can use our own database to retrieve and store out TodoItem if we want, but for simplicity we just use in memory store.

router.get("/todos", handler: { (completion: @escaping ([TodoItem]?, RequestError?) -> Void) in
    completion(todos, nil)
})

You can test the request using cURL command from the terminal. It should return list of TodoItem already encoded to the JSON representation.

curl -X GET localhost:8090/todos

Retrieve Single TodoItem

To retrieve single ToDoItem, we need to add additional parameter, id which is a String, also change the completion closure parameter to optional TodoItem. We check if the id passed belongs to one of our TodoItem inside our todos array. If yes we invoke the completion passing the TodoItem object, if not we pass the RequestError.notFound Error.

router.get("/todos") { (id: String, completion: @escaping (TodoItem?, RequestError?) -> Void) in
    let todo = todos.first { $0.id.contains(id) }
    if let todo = todo {
        completion(todo, nil)
    } else {
        completion(nil, RequestError.notFound)
    }
}

To test retrieving single TodoItem, we need to append the id as the last path component of our URL.

curl -X GET localhost:8090/todos/1

Create TodoItem

To create item we use the POST method and /todos/create as the path. We need to add TodoItem as the parameter for the handler closure. The completion will return either TodoItem in case of TodoItem successfully created or RequestError if an error occurs. Inside the function, we check is the passed TodoItem Id already exists inside our in memory array. If not, we just append it to the array and invoke completion closure passing the TodoItem.

router.post("/todos/create") { (todo: TodoItem, completion: (TodoItem?, RequestError?) -> Void) in
    let currentTodoIndex = todos.index { $0.id.contains(todo.id) }
    if currentTodoIndex != nil {
        completion(nil, RequestError.conflict)
    } else {
        todos.append(todo)
        completion(todo, nil)
    }
}

To test create TodoItem, we can use curl command, set the Header to accept JSON and sending the JSON data in our request body.

curl -X POST localhost:8090/todos/create \
-H "Content-Type:application/json" \ 
-d '{
   "id": "56789",
   "title": "Hello",
   "detail": "world",
}'

Delete TodoItem

To delete the TodoItem, we add id parameter with the type of String, and completion of optional RequestError in case the deletion fails or no TodoItem found with the passed id. Inside the function, we just use the id to find the current TodoItem index that match the passed id. If found, we just remove the TodoItem at that index.

router.delete("/todos") { (id: String, completion: (RequestError?) -> Void) in
    let currentTodoIndex = todos.index { $0.id.contains(id) }
    if let currentTodoIndex = currentTodoIndex {
        todos.remove(at: currentTodoIndex)
        completion(nil)
    } else {
        completion(RequestError.notFound)
    }
}

Here is the curl command to delete a TodoItem.

curl -X DELETE localhost:8090/todos/1

Here is the full source code.

import Kitura
import HeliumLogger
import LoggerAPI
import KituraContracts

struct TodoItem: Codable {
    var id: String
    var title: String
    var detail: String
}

HeliumLogger.use()
let router = Router()

var todos = [
    TodoItem(id: "1", title: "Clean the house", detail: "My house"),
    TodoItem(id: "2", title: "Wash your car", detail: "Your personal car")
]

router.get("/todos") { (completion: @escaping ([TodoItem]?, RequestError?) -> Void) in
     completion(todos, nil)
}

router.get("/todos") { (id: String, completion: @escaping (TodoItem?, RequestError?) -> Void) in
    let todo = todos.first { $0.id.contains(id) }
    if let todo = todo {
        completion(todo, nil)
    } else {
        completion(nil, RequestError.notFound)
    }
}

router.post("/todos/create") { (todo: TodoItem, completion: @escaping (TodoItem?, RequestError?) -> Void) in
    let currentTodoIndex = todos.index { $0.id.contains(todo.id) }
    if currentTodoIndex != nil {
        completion(nil, RequestError.conflict)
    } else {
        todos.append(todo)
        completion(todo, nil)
    }
}

router.post("/todos/edit") { (todo: TodoItem, completion: @escaping (TodoItem?, RequestError?) -> Void) in
    let currentTodoIndex = todos.index { $0.id.contains(todo.id) }
    if let currentTodoIndex = currentTodoIndex {
        todos[currentTodoIndex] = todo
        completion(todo, nil)
    } else {
        completion(nil, RequestError.notFound)
    }
}

router.delete("/todos") { (id: String, completion: @escaping (RequestError?) -> Void) in
    let currentTodoIndex = todos.index { $0.id.contains(id) }
    if let currentTodoIndex = currentTodoIndex {
        todos.remove(at: currentTodoIndex)
        completion(nil)
    } else {
        completion(RequestError.notFound)
    }
}

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

Conclusion

Kitura Codable Router really simplify the JSON Encoding and Decoding logic for the Server Side Swift. Server Side Swift future is very promising with Apple just releasing the SwiftNIO event driven low level backend framework with non blocking IO operation. Kitura team is already showing an initiative to implement it in the future.