Tutorial: write a CRUD API using JSON
At the end of this tutorial you will know how to convert a Model into JSON and back and how to built an API that can Create, Read, Update and Delete (CRUD) a User using JSON! π
You can find the result of this tutorial on github here
This tutorial is a natural follow-up of How to write Controllers. You can either go for that tutorial first and come back later or be a rebel, skip it and read on π
1. Create a new project
We will use the outcome of the aforementioned tutorial as a template to create our new project:
vapor new projectName --template=vaporberlin/my-first-controller --branch=vapor-2
2. Generate Xcode project
Before we generate an Xcode project we would have to change the package name within Package.swift and remove one dependency that we wont need:
// swift-tools-version:4.0import PackageDescriptionlet package = Package(
name: "projectName", // changed
products: [
.library(name: "App", targets: ["App"]),
.executable(name: "Run", targets: ["Run"])
],
dependencies: [
.package(url: "https://github.com/vapor/vapor.git", .upToNextMajor(from: "2.1.0")),
.package(url: "https://github.com/vapor/leaf-provider.git", .upToNextMajor(from: "1.1.0")), // deleted
.package(url: "https://github.com/vapor/fluent-provider.git", .upToNextMajor(from: "1.3.0"))
],
targets: [
.target(name: "App", dependencies: ["Vapor", "LeafProvider", "FluentProvider"], // deleted LeafProvider
exclude: [
"Config",
"Public",
"Resources",
]
),
.target(name: "Run", dependencies: ["App"]),
.testTarget(name: "AppTests", dependencies: ["App", "Testing"])
]
)
Now in the terminal at the root directory projectName/ execute:
vapor update -y
It may take a bit fetching the dependency, but when done you should have a project structure like this:
projectName/
βββ Package.swift
βββ Sources/
β βββ App/
β β βββ Controllers/
β β β βββ UserController.swift
β β βββ Models/
β β β βββ User.swift
β β βββ Routes/
β β β βββ Routes.swift
β β βββ Setup/
β β βββ Config+Setup.swift
β β βββ Droplet+Setup.swift
β βββ Run/
βββ Tests/
βββ Config/
βββ Resources/
βββ Public/
βββ Dependencies/
βββ Products/
3. Delete unnecessary files / code
Weβd need to clean up some files and lines of code before we actually begin since the template used the leaf-provider and some leaf specific things are not needed anymore - in Setup/Config+Setup.swift delete the LeafProvider:
import LeafProvider // delete
import FluentProviderextension Config {
public func setup() throws {
try setupProviders()
try setupPreparations()
} /// Configure providers
private func setupProviders() throws {
try addProvider(LeafProvider.Provider.self) // delete
try addProvider(FluentProvider.Provider.self)
} /// Add all models that should have their
/// schemas prepared before the app boots
private func setupPreparations() throws {
preparations.append(User.self)
}
}
Okay now to our Routes/Routes.swift:
import Vaporextension Droplet {
func setupRoutes() throws {
let userController = UserController() // changed
get("user", handler: userController.list)
post("user", handler: userController.create)
}
}
Next to our Controllers/UserController.swift - we will remove the drop completely since we donβt need it anymore and also simplify one return value. We will come back here when implementing our API. But for now do:
final class UserController {
// delete the drop and init() func list(_ req: Request) throws -> ResponseRepresentable {
let list = try User.all()
return "alohomora" // changed
} func create(_ req: Request) throws -> ResponseRepresentable {
...
}
}
We are almost there. In our Models/User.swift:
import Vapor
import FluentProviderfinal class User: Model {
...
}// MARK: Fluent Preparationextension User: Preparation {
...
}// delete the implementation 'extension User: NodeRepresentable {}'
And two last things to delete (move to trash). First the Resources/ directory:
projectName/
βββ Package.swift
βββ Sources/
β βββ App/
β β βββ Config+Setup.swift
β β βββ Droplet+Setup.swift
β β βββ Routes.swift
β β βββ Models/
β β β βββ User.swift
β β βββ Controllers/
β β βββ UserController.swift
β βββ Run/
βββ Tests/
βββ Config/
βββ Resources/ // delete
β βββ Views/
β βββ userview.leaf
βββ Public/
βββ Dependencies/
βββ Products/
And secondly in our Config/droplet.json delete all the marked lines:
{
... "//": "The type of view renderer that drop.view will use",
"//": "leaf: Pure Swift templating language created for Vapor.",
"//": "static: Simply return the view at the supplied path",
"view": "leaf", ...
}
If you now cmd+r or run and fire up the /user route you should see an iconic spell β¨ !
4. Make your model JSONConvertible
This is going to be super easy, all we need to do is extend our User by JSONConvertible and implement two things. In our Models/User.swift:
import Vapor
import FluentProviderfinal class User: Model {
...
}// MARK: Fluent Preparationextension User: Preparation {
...
}// MARK: JSONextension User: JSONConvertible { convenience init(json: JSON) throws {
self.init(username: try json.get("username"))
}
func makeJSON() throws -> JSON {
var json = JSON()
try json.set("id", assertExists())
try json.set("username", username)
return json
}
}
With convenience init() we just define another possibility to initiate our user. Inside here we are actually calling our normal init() and pass the string we try to get from the json. With that we are able to initiate a new user by passing in a json. Now that is convenient. π
With makeJSON() we define the representation of our user as a json object. All we do is initiating a new json object, setting the username variable at key βusernameβ and returning our json. We are all prepared now. ππ»
5. Implement: CREATE
In our Controllers/UserController.swift adjust the create() function to:
final class UserController { func create(_ req: Request) throws -> ResponseRepresentable { guard let json = req.json else {
return "missing json"
} var user: User
do { user = try User(json: json) }
catch { return "could not initiate user with given json" } try user.save()
return try user.makeJSON()
} func list(_ req: Request) throws -> ResponseRepresentable {
...
}
Let me for short explain what we do here. The first guard let statement is quite clear with checking if the request includes json and in case it does not - we return a string.
The do-catch lines actually take advantage of our convenient init() within our User.swift to initiate a user by passing in a json. Since it could fail if the json does not provide the needed fields (in our case βusernameβ) we want to catch that case and handle it by just returning another string.
At the end weβre just saving the user to our in memory database and using makeJSON() to return him as a json - thatβs it!
NOTE: Firing up a route from the url-section in your browser is always, always, always a GET-request. In order to make a POST-request (we defined that for our create() function within Routes.swift) you would need (if you prefer a GUI) a tool like Postman or Paw. But if you want to be nerdy like me you can just use your Terminal with a native command called curl. This tutorial uses curl. π€
5.1 CURL - Overview
Letβs have a look on how the curl command for our POST-request looks like:
curl -H "Content-Type: application/json" -X POST -d '{"username":"thewoodwalker"}' http://127.0.0.1:8006/user
There are only four parts here. Itβs easier than it probably appears!
β’ With -H we are setting a HEADER with βContent-Type: application/jsonβ
β’ With -X we are defining the HTTP-Method we want to fire (POST here)
β’ With -d we are saying that next follows our data we want to send
β’ A curl command always ends with the url you want to fire the request to
Since curl does not recognize what kind of data we want to send after -d we need to define it within the header (just as we do). If you donβt define a header, curl will by default send your data form-encoded. That is the same encoding used when you submit a form on a website βπ»π€.
Now cmd+r or run your project and check your Xcode Console for the port:
The current hash key "0000000000000000" is not secure.
Update hash.key in Config/crypto.json before using in production.
Use `openssl rand -base64 <length>` to generate a random string.
The current cipher key "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" is not secure.
Update cipher.key in Config/crypto.json before using in production.
Use `openssl rand -base64 32` to generate a random string.
Database prepared
No command supplied, defaulting to serve...
Starting server on 0.0.0.0:8006 // my project runs on port 8006
Letβs fire our first curl request in our terminal to create a user:
curl -H "Content-Type: application/json" -X POST -d '{"username":"thewoodwalker"}' http://127.0.0.1:8006/user
It should return:
{"id":1, "username":"thewoodwalker"}
I found that so cool when I learned it! I really feel like knowing how to make a request without the need of installing another app is a small super power!
6. Implement: READ
What comes next is too easy π! In our Controllers/UserController.swift just do:
final class UserController { func create(_ req: Request) throws -> ResponseRepresentable {
...
} func list(_ req: Request) throws -> ResponseRepresentable {
let list = try User.all()
return try list.makeJSON() // changed
}
}
Puh.. that was exhausting. Now cmd+r or run and fire up your /user route!
Actually.. we wont see anything hahah π
- we have an in memory database that resets every time we rerun. Just make a curl request for creating a new user just as we learned it and fire up the /user route again π.
7. Implement: UPDATE
In our Routes.swift weβll add a new route that requires an id in the url:
import Vaporextension Droplet {
func setupRoutes() throws {
let userController = UserController()
get("user", handler: userController.list)
post("user", handler: userController.create)
patch("user", ":id", handler: userController.update) // added
}
}
I know we donβt have an update() function within our UserController.swift yet, but weβll come to it in a second. The more interesting thing here I think is the β:idβ part in our patch route definition. With that we define that it needs a PATCH-request to a route looking like β/user/somethingβ in order to get triggered. In our create() function we will make sure that this something at position of :id is of type Int. In our Controllers/UserController.swift do:
final class UserController { func create(_ req: Request) throws -> ResponseRepresentable {
...
} func list(_ req: Request) throws -> ResponseRepresentable {
...
} func update(_ req: Request) throws -> ResponseRepresentable { guard let userId = req.parameters["id"]?.int else {
return "no user id provided"
} guard let json = req.json else {
return "missing json"
} guard let user = try User.find(userId) else {
return "could not find user with id \(userId)"
} // set username if the field exists else reassign old value
user.username = try json.get("username") ?? user.username
try user.save()
return try user.makeJSON()
}
}
In the first guard let statement weβre grabbing the value at position id and trying to get it as an Int and if it fails weβll return a string. I think (hope) the rest of the function is so clear that no further explanation is needed π!
NOTE: If you still have any question donβt hesitate to write a comment and ask!
Now letβs rerun the project, fire a request with the curl to create a new user and remember the id that will be in the returned json for our now coming PATCH-request:
curl -H "Content-Type: application/json" -X PATCH -d '{"username":"emmastone"}' http://127.0.0.1:8006/user/1
It basically looks just like the POST request except that we now have PATCH as our HTTP method and send a different value for username to see a difference π
8. Implement: DELETE
Our final chapter - in our Routes.swift add:
import Vaporextension Droplet {
func setupRoutes() throws {
let userController = UserController()
get("user", handler: userController.list)
post("user", handler: userController.create)
patch("user", ":id", handler: userController.update)
delete("user", ":id", handler: userController.delete) // added
}
}
Iβm not super sure.. but somehow have the feeling youβre an expert with that kind of route definition.. π€ In our Controllers/UserController.swift add:
final class UserController { func create(_ req: Request) throws -> ResponseRepresentable {
...
} func list(_ req: Request) throws -> ResponseRepresentable {
...
} func update(_ req: Request) throws -> ResponseRepresentable {
...
} func delete(_ req: Request) throws -> ResponseRepresentable { guard let userId = req.parameters["id"]?.int else {
return "no user id provided"
} guard let user = try User.find(userId) else {
return "could not find user with id \(userId)"
} try user.delete()
return Response(status: .ok)
}
}
Our final cmd+r or run and once again create a user with a curl request, maybe have a look at /user in your browser and then delete that user with:
curl -X DELETE http://127.0.0.1:8006/user/1
And thatβs it! You successfully implemented a CRUD API using JSON π !