Tutorial: write a CRUD API using JSON

Martin Lasek
Nov 5, 2017 ยท 9 min read
Image for post
Image for post

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 ๐Ÿ˜Š

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

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/

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 FluentProvider
extension 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 FluentProvider
final 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 โœจ !

Image for post
Image for post
Note: make sure to select Run as a scheme next to your button before running the app

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 FluentProvider
final 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. ๐Ÿ™Œ๐Ÿป

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!

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 ๐Ÿ˜Š.

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 ๐Ÿ˜‰

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 ๐ŸŽ‰ !


Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch

Follow all the topics you care about, and weโ€™ll deliver the best stories for you to your homepage and inbox. Explore

Get unlimited access to the best stories on Medium โ€” and support writers while youโ€™re at it. Just $5/month. Upgrade

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store