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

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.0
import PackageDescription
let 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/
β”œβ”€β”€ 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 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 Vapor
extension 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 Preparation
extension 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 ✨ !

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

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 FluentProvider
final class User: Model {
...
}
// MARK: Fluent Preparation
extension User: Preparation {
...
}
// MARK: JSON
extension 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!

8. 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 😊.

9. Implement: UPDATE

In our Routes.swift we’ll add a new route that requires an id in the url:

import Vapor
extension 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 πŸ˜‰

10. Implement: DELETE

Our final chapter - in our Routes.swift add:

import Vapor
extension 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 πŸŽ‰Β !


Thank you a lot for reading! If you have any questions or improvementsβ€Šβ€”β€Šwrite a comment! I would love to hear from you! 😊