We want to develop a very simple backend for a Todo application.
We will start by creating our model. The code is as follows:
import Vapor final class Todo { var id: Int var name: String var description: String var notes: String var completed: Bool var synced: Bool init(id: Int, name: String, description: String, notes: String, completed: Bool, synced: Bool) { self.id = id self.name = name self.description = description self.notes = notes self.completed = completed self.synced = synced } }
This class imports Vapor and includes some of the Todo
-related properties as well as an init
method.
To be able to pass this model into JSON arrays and dictionaries, we need to extend a protocol called JsonRepresentable
:
extension Todo: JSONRepresentable { func makeJson() -> JSON { return JSON([ "id":id, "name": "(name)", "description": "(description)", "notes": "(notes)", "completed": completed, "synced": synced ]) } }
Then we want to store list of Todo items in memory. To be able to achieve this, we will create a new class called TodoStore
. The code is as follows:
import Vapor final class TodoStore { static let sharedInstance = TodoStore() private var list: [Todo] = Array<Todo>() private init() { } }
For the sake of simplicity, we make this class a singleton that stores a list of Todo items. Also, we make the init
method private
to avoid non-shared instance initiation.
To allow instances of Todo to be passed into JSON arrays and dictionaries as if it were a native JSON type, we will need to extend our TodoStore
by conforming to JSONRepresentable
as follows:
extension TodoStore: JSONRepresentable { func makeJson() -> JSON { return JSON([ "list": "(list)" ]) } }
Next, we add the following methods:
func addtem(item: Todo) { self.list.append(item) } func listItems() -> [Todo] { return self.list }
As the names suggest, these methods will be used for adding and listing items. We will need a very simple find method, so let's develop it:
func find(id: Int) -> Todo? { return self.list.index { $0.id == id }.map { self.list[$0] } }
Here, we use index
and map
higher-order functions to find the index and return the respective array element.
Then, we will need to develop update
and delete
methods:
func delete(id: Int) -> String { if self.find(id: id) != nil { self.list = self.list.filter { $0.id != id } return "Item is deleted" } return "Item not found" } func deleteAll() -> String { if self.list.count > 0 { self.list.removeAll() return "All items were deleted" } return "List was empty" } func update(item: Todo) -> String { if let index = (self.list.index { $0.id == item.id }) { self.list[index] = item return "item is up to date" } return "item not found" }
Also, we can combine add and update as follows:
func addOrUpdateItem(item: Todo) { if self.find(item.id) != nil { update(item) } else { self.list.append(item) } }
At this point, our TodoStore
is capable of all CRUD operations.
The next step will be developing routing, request, and response handling. For the sake of simplicity, we will modify main.swift
in the Vapor example.
We will need to make our changes after the following definition:
let app = Application()
The first step will be to develop a post method to create a Todo item as follows:
/// Post a todo item app.post("postTodo") { request in guard let id = request.headers.headers["id"]?.values, name = request.headers.headers["name"]?.values, description = request.headers.headers["description"]?.values, notes = request.headers.headers["notes"]?.values, completed = request.headers.headers["completed"]?.values, synced = request.headers.headers["synced"]?.values else { return JSON(["message": "Please include mandatory parameters"]) } let todoItem = Todo(id: Int(id[0])!, name: name[0], description: description[0], notes: notes[0], completed: completed[0].toBool()!, synced: synced[0].toBool()!) let todos = TodoStore.sharedInstance todos.addOrUpdateItem(item: todoItem) let json:[JSONRepresentable] = todos.listItems().map { $0 } return JSON(json) }
The preceding example is going to create a Todo item. First, we check if the API user is provided with all the necessary HTTP headers with a guard expression and then we use our addItem()
method in the TodoStore
class to add that specific item. In the preceding code example, we needed to convert completed
from Bool
to String
, so we extended the String
function as follows and we called toBool()
on completed
:
extension String { func toBool() -> Bool? { switch self { case "True", "true", "yes", "1": return true case "False", "false", "no", "0": return false default: return nil } } }
We will need to build and run our backend app with the vapor build
and vapor run
directives in the terminal application. At this point, we should get the following prompt:
If we point to localhost 8080 in a web browser, we should see Vapor up and running. Also, we can use the curl tool to test our post method in the terminal by copying and pasting the following code:
curl -X "POST" "http://localhost:8080/postTodo/" -H "Cookie: test=123" -H "id: 3" -H "notes: do not forget to buy potato chips" -H "Content-Type: application/json" -H "description: Our first todo item" -H "completed: false" -H "name: todo 1" -d "{}"
The result will resemble the following:
As we can see from the screenshot, we received a JSON response that includes our added Todo item.
Our post call returns the list of items. Also, we can get items with this:
/// List todo items app.get("todos") { request in let todos = TodoStore.sharedInstance let json:[JSONRepresentable] = todos.listItems().map { $0 } return JSON(json) }
We will build and run our application with Vapor CLI again and we can test this get request like this:
curl -X "GET" "http://localhost:8080/todos" -H "Cookie: test=123"
The preceding call retrieves all the items. If we want to get a specific item, we can do that too:
/// Get a specific todo item app.get("todo") { request in guard let id = request.headers.headers["id"]?.values else { return JSON(["message": "Please provide the id of todo item"]) } let todos = TodoStore.sharedInstance.listItems() var json = [JSONRepresentable]() let item = todos.filter { $0.id == Int(id[0])! } if item.count > 0 { json.append(item[0]) } return JSON(json) }
Here, we check for the existence of headers and use the listItems()
method in our TodoStore
class to retrieve that specific item. We can test it in curl by executing the following commands in the terminal:
curl -X "GET" "http://localhost:8080/todo/" -H "id: 1" -H "Cookie: test=123"
The next operation that we need to implement is deleting items from our TodoStore
. Let's implement the delete
and deleteAll
methods:
/// Delete a specific todo item app.delete("deleteTodo") { request in guard let id = request.headers.headers["id"]?.values else { return JSON(["message": "Please provide the id of todo item"]) } let todos = TodoStore.sharedInstance todos.delete(id: Int(id[0])!) return JSON(["message": "Item is deleted"]) } /// Delete all items app.delete("deleteAll") { request in TodoStore.sharedInstance.deleteAll() return JSON(["message": "All items are deleted"]) }
To test the delete functionality, we can execute the following commands in the terminal:
curl -X "DELETE" "http://localhost:8080/deleteTodo/" -H "id: 1" -H "Cookie: test=123"
To test the deleteAll
functionality, we can execute the following commands in the terminal:
curl -X "DELETE" "http://localhost:8080/deleteAll" -H "Cookie: test=123"
Finally, we want to be able to update an item in our Todo list to complete it or take some notes:
/// Update a specific todo item app.post("updateTodo") { request in guard let id = request.headers.headers["id"]?.values, name = request.headers.headers["name"]?.values, description = request.headers.headers["description"]?.values, notes = request.headers.headers["notes"]?.values, completed = request.headers.headers["completed"]?.values, synced = request.headers.headers["synced"]?.values else { return JSON(["message": "Please include mandatory parameters"]) } let todoItem = Todo(id: Int(id[0])!, name: name[0], description: description[0], notes: notes[0], completed: completed[0].toBool()!, synced: synced[0].toBool()!) let todos = TodoStore.sharedInstance todos.update(item: todoItem) return JSON(["message": "Item is updated"]) }
Here, we check for the headers first and, if they are present, we use the update method in TodoStore
to update a specific item in our store. We can test it like this:
curl -X "POST" "http://localhost:8080/updateTodo" -H "Cookie: test=123" -H "id: 3" -H "notes: new note" -H "name: updated name" -H "description: updated description" -H "completed : yes"
At this point, we should have a simple backend API to create, list, update, and delete todo items in memory. In the next section, we will develop an iOS application to leverage this API.