This tutorial teaches how to create a Kitura backend for the Todo-Backend project, which provides tests and a web client for a "To Do List" application.
Note: This workshop has been developed for Swift 4, Xcode 9.x and Kitura 2.x.
-
Install the Kitura CLI:
- Configure the Kitura homebrew tap
brew tap ibm-swift/kitura
- Install the Kiura CLI from homebrew
brew install kitura
- Configure the Kitura homebrew tap
-
Clone this project from GitHub to your machine (don't use the Download ZIP option):
cd ~ git clone http://github.com/IBM/ToDoBackend
-
Clone the ToDo Backend tests from GitHub to your machine (don't use the Download ZIP option):
cd ~ git clone http://github.com/TodoBackend/todo-backend-js-spec
In order to implement a To Do Backend, a server is required that provides support for storing, retrieving, deleting and updating "to do" items. The To Do Backend project doesn't provide a specification as such for how the server must respond, rather it provides a set of tests which the server must pass. The "todo-backend-js-spec" project provides those tests.
The following steps allow you to run the tests:
- Open the tests in a web browser:
This should open your browser with the test page open.
cd ~/todo-backend-js-spec open index.html
- Set a
test target root
of http://localhost:8080 - Click
run tests
The first error reported should be as follows:
❌ the api root responds to a GET (i.e. the server is up and accessible, CORS headers are set up)
AssertionError: expected promise to be fulfilled but it was rejected with [Error:
GET http://localhost:8080
FAILED
The browser failed entirely when make an AJAX request.
This shows that the tests made a GET
request to http://localhost.com:8080
, but that it failed with no response, which is expected as there is no server.
In the instructions below, reloading the page will allow you to re-run the ToDo Backend tests.
Implementing a compliant ToDo Backend is an incremental task, with the aim at each step to pass more tests. The first step is to create a Kitura server to response on requests.
-
Create a directory for the server project
cd ~/ToDoBackend mkdir ToDoServer cd ToDoServer
-
Create a Kitura starter project
kitura init
The Kitura CLI will now create and build an starter Kitura application for you. This includes adding best-practice implementations of capabilities such as configuration, health checking and monitoring to the application for you.
More information about the project structure is available on kitura.io.
-
Open the ToDoServer project in Xcode
cd ~/ToDoBackend/ToDoServer open ToDoServer.xcodeproj
-
Run the server project in Xcode
- Change the selected target from "ToDoServer-Package" to the "TodoServer" executable.
2. Press the
Run
button or use the⌘+R
key shortcut. 3. SelectAllow incoming network connections
if you are prompted.
- Change the selected target from "ToDoServer-Package" to the "TodoServer" executable.
2. Press the
-
Check that some of the standard Kitura URLs are running:
- Kitura Monitoring: http://localhost:8080/swiftmetrics-dash/
- Kitura Health check: http://localhost:8080/health
- Kitura splash screen: http://localhost:8080/
-
Rerun the tests by reloading the test page in the browser.
The first test should fail with the following:
❌ the api root responds to a GET (i.e. the server is up and accessible, CORS headers are set up)
AssertionError: expected promise to be fulfilled but it was rejected with [Error:
GET http://localhost:8080/
FAILED
The browser failed entirely when make an AJAX request.
Either there is a network issue in reaching the url, or the
server isn't doing the CORS things it needs to do.
This test is still failing, even though the server is responding on localhost:8080
. This is because Cross Origin Resource Sharing (CORS) is not enabled.
By default, web servers only serve content to web pages that were served by that web server. In order to allow other web pages, such as the ToDo Backend test page, to connect to the server, Cross Origin Resource Sharing (CORS) must be enabled.
-
Add the CORS library to the
ToDoServer
>Package.swift
fileAdd the following to the end of the dependencies section of the Package.swift file:
.package(url: "https://github.com/IBM-Swift/Kitura-CORS", .upToNextMinor(from: "2.1.0")),
and update the dependencies line for the Application target to the following:
.target(name: "Application", dependencies: [ "Kitura", "KituraCORS", "Configuration", "CloudEnvironment", "Health" , "SwiftMetrics", ]),
NOTE:- In order for Xcode to pick up the new dependency, the Xcode project now needs to be regenerated.
-
Close Xcode, regenerate the Xcode project and reopen:
cd ~/ToDoBackend/ToDoServer swift package generate-xcodeproj open ToDoServer.xcodeproj
-
Open the
Sources
>Application
>Application.swift
file -
Add an import for the CORS library to the start of the file:
import KituraCORS
-
Add the following into the start of the
postInit()
function:let options = Options(allowedOrigin: .all) let cors = CORS(options: options) router.all("/*", middleware: cors)
-
Re-run the server project in Xcode
- Edit the scheme and select a Run Executable of “ToDoServer”
2. Run the project, thenAllow incoming network connections
if you are prompted.
- Edit the scheme and select a Run Executable of “ToDoServer”
-
Rerun the tests by reloading the test page in the browser.
The first test should now be passing but the second test is failing:
❌ the api root responds to a POST with the todo which was posted to it
In order to fix this, we need to implement a POST
request that saves a todo item.
REST APIs typically consist of a HTTP request using a verb such as POST
, PUT
, GET
or DELETE
along with a URL and an optional data payload. The server then handles the request and responds with an optional data payload.
A request to store data typically consists of a POST request with the data to be stored, which the server then handles and responds with a copy of the data that has just been stored. This means we need to define a ToDo type, register a handler for POST requests on /
, and implement the handler to store the data.
-
Define a data type for the ToDo items:
- Select the Application folder in the left hand explorer in Xcode
2. Select
File
>New
>File...
from the pull down menu 3. Select Swift File and clickNext
4. Name the fileModels.swift
, change theTargets
fromToDoServerPackageDescription
toApplication
, then clickCreate
5. Add the following to the created file:
public struct ToDo : Codable, Equatable { public var id: Int? public var user: String? public var title: String? public var order: Int? public var completed: Bool? public var url: String? public static func ==(lhs: ToDo, rhs: ToDo) -> Bool { return (lhs.title == rhs.title) && (lhs.user == rhs.user) && (lhs.order == rhs.order) && (lhs.completed == rhs.completed) && (lhs.url == rhs.url) && (lhs.id == rhs.id) } }
This creates a struct for the ToDo items that uses Swift 4's
Codable
capabilities. - Select the Application folder in the left hand explorer in Xcode
2. Select
-
Create an in-memory data store for the ToDo items 1. Open the
Sources
>Application
>Application.swift
file 2. Add atodoStore
,nextId
and aworkerQueue
into the App class. On the line belowlet cloudEnv = CloudEnv()
add:private var todoStore = [ToDo]() private var nextId :Int = 0 private let workerQueue = DispatchQueue(label: "worker")
- To be able to use
DispatchQueue
on Linux, add the followingimport
statement to the start of the file:
import Dispatch
- Add a helper method at the end of the class, before the last closing brace
func execute(_ block: (() -> Void)) { workerQueue.sync { block() } }
This will be used to make sure that access to shared resources is serialized so the app does not crash on concurrent requests.
- To be able to use
-
Register a handler for a
POST
request on/
that stores the ToDo item data- Add the following into the
postInit()
function:
router.post("/", handler: storeHandler)
- Implement the storeHandler that receives a ToDo, and returns the stored ToDo
Add the following as a function in the App class:
func storeHandler(todo: ToDo, completion: (ToDo?, RequestError?) -> Void ) { var todo = todo if todo.completed == nil { todo.completed = false } todo.id = nextId todo.url = "http://localhost:8080/\(nextId)" nextId += 1 execute { todoStore.append(todo) } completion(todo, nil) }
- Add the following into the
This expects to receive a ToDo struct from the request, sets completed
to false if it is nil
and adds a url
value that informs the client how to retrieve this todo item in the future.
The handler then returns the updated ToDo item to the client.
- Run the project and rerun the tests by reloading the test page in the browser.
The first three tests should now pass and the fourth fails:
:X: after a DELETE the api root responds to a GET with a JSON representation of an empty array
In order to fix this, a handler for DELETE
requests and a subsequent GET
handler to return the stored ToDo items.
A request to delete data typically consists of a DELETE request. If the request is to delete a specific item, a URL encoded identifier is normally provided (eg. '/1' for the item with ID 1). If no identifier is provided, it is a request to delete all of the items.
In order to pass the next test, the ToDoServer needs to handle a DELETE
on /
resulting in removing all stored ToDo items.
- Register a handler for a
DELETE
request on/
that empties the ToDo item data- Add the following into the
postInit()
function:
router.delete("/", handler: deleteAllHandler)
- Add the following into the
2. Implement the deleteAllHandler
empties the todoStore
Add the following as a function in the App class:
func deleteAllHandler(completion: (RequestError?) -> Void ) {
execute {
todoStore = [ToDo]()
}
completion(nil)
}
A request to load all of the stored data typically consists of a GET
request with no data, which the server then handles and responds with an array of the data that has just been stored.
- Register a handler for a
GET
request on/
that loads the data
Add the following into thepostInit()
function:router.get("/", handler: getAllHandler)
- Implement the
getAllHandler
that responds with all of the stored ToDo items as an array. Add the following as a function in the App class:func getAllHandler(completion: ([ToDo]?, RequestError?) -> Void ) { completion(todoStore, nil) }
- Run the project and rerun the tests by reloading the test page in the browser.
The first seven tests should now pass, with the eighth test failing:
❌ each new todo has a url, which returns a todo
GET http://localhost:8080/0
FAILED
404: Not Found (Cannot GET /0.)
The failing test is trying to load a specific ToDo item by making a GET
request with the ID of the ToDo item that it wishes to retrieve, which is based on the ID in the url
field of the ToDo item set when the item was stored by the earlier POST
request. In the test above the reqest was for GET /0
- a request for id 0.
Kitura's Codable Routing is able to automatically convert identifiers used in the GET
request to a parameter that is passed to the registered handler. As a result, the handler is registered against the /
route, with the handler taking an extra parameter.
-
Register a handler for a
GET
request on/
:router.get("/", handler: getOneHandler)
-
Implement the
getOneHandler
that receives anid
and responds with a ToDo item:func getOneHandler(id: Int, completion: (ToDo?, RequestError?) -> Void ) { completion(todoStore.first(where: {$0.id == id }), nil) }
-
Run the project and rerun the tests by reloading the test page in the browser.
The first nine tests now pass. The tenth fails with the following:
❌ can change the todo's title by PATCHing to the todo's url
PATCH http://localhost:8080/0
FAILED
404: Not Found (Cannot PATCH /0.)
The failing test is trying to PATCH
a specific ToDo item. A PATCH
request updates an existing item by updating any fields sent as part of the PATCH
request. This means that a field by field update needs to be done.
- Register a handler for a
PATCH
request on/
:
router.patch("/", handler: updateHandler)
- Implement the
updateHandler
that receives anid
and responds with the updated ToDo item:func updateHandler(id: Int, new: ToDo, completion: (ToDo?, RequestError?) -> Void ) { guard let idMatch = todoStore.first(where: { $0.id == id }), let idPosition = todoStore.index(of: idMatch) else { return } var current = todoStore[idPosition] current.user = new.user ?? current.user current.order = new.order ?? current.order current.title = new.title ?? current.title current.completed = new.completed ?? current.completed execute { todoStore[idPosition] = current } completion(todoStore[idPosition], nil) }
- Run the project and rerun the tests by reloading the test page in the browser.
Twelve tests should now be passing, with the thirteenth failing as follows:
:x: can delete a todo making a DELETE request to the todo's url
DELETE http://localhost:8080/0
FAILED
404: Not Found (Cannot DELETE /0.)
The failing test is trying to DELETE
a specific ToDo item. This means registering an additional handler for DELETE
that this time accepts an ID as a parameter.
- Register a handler for a
DELETE
request on/
:router.delete("/", handler: deleteOneHandler)
- Implement the
deleteOneHandler
that receives anid
and removes the specified ToDo item:func deleteOneHandler(id: Int, completion: (RequestError?) -> Void ) { guard let idMatch = todoStore.first(where: { $0.id == id }), let idPosition = todoStore.index(of: idMatch) else { return } execute { todoStore.remove(at: idPosition) } completion(nil) }
- Run the project and rerun the tests by reloading the test page in the browser.
All sixteen tests should now be passing!
Congratulations, you've built a Kitura backend for the Todo-Backend project!
This tutorial has helped you build a ToDo Backend for the web tests and web client from the Todo-Backend project, but one of the great values of Swift is end to end development between iOS and the server. Clone the iOSSampleKituraKit repository and open the iOSKituraKitSample.xcworkspace
to see a iOS app client for the ToDo-Backend project.
cd ~
git clone https://github.com/IBM-Swift/iOSSampleKituraKit.git
cd iOSSampleKituraKit/KituraiOS
open iOSKituraKitSample.xcworkspace/
Run (⌘+R
) the iOS application. You should be able to use the app to add, change and delete ToDo items!