Tutorial: write a CRUD API using JSON

Martin Lasek
8 min readMay 26, 2018

--

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 😊

Index

1. Create a new project
2. Generate Xcode project
3. Delete unnecessary files / code
4. The READ route
5. The CREATE route
6. The UPDATE route
7. The DELETE route
8. Where to go from here

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
dependencies: [
// πŸ’§ A server-side Swift web framework.
.package(url: "https://github.com/vapor/vapor.git", from: "3.0.0-rc"),
.package(url: "https://github.com/vapor/leaf.git", from: "3.0.0-rc"), // deleted
.package(url: "https://github.com/vapor/fluent-sqlite.git", from: "3.0.0-rc")
],
targets: [
.target(name: "App", dependencies: ["Vapor", "Leaf", "FluentSQLite"]), // deleted Leaf
.target(name: "Run", dependencies: ["App"]),
.testTarget(name: "AppTests", dependencies: ["App"]),
]
)

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
β”‚ β”‚ β”œβ”€β”€ app.swift
β”‚ β”‚ β”œβ”€β”€ boot.swift
β”‚ β”‚ β”œβ”€β”€ configure.swift
β”‚ β”‚ └── routes.swift
β”‚ └── Run/
β”‚ └── main.swift
β”œβ”€β”€ Tests/
β”œβ”€β”€ Resources/
β”‚ └── Views/
β”‚ └── userview.leaf
β”œβ”€β”€ 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 our project is based on used leaf and some leaf specific things are not needed anymore. In configure.swift delete:

import Vapor
import Leaf // delete
import FluentSQLite
/// Called before your application initializes.
public func configure(
_ config: inout Config,
_ env: inout Environment,
_ services: inout Services
) throws {
/// Register routes to the router
let router = EngineRouter.default()
try routes(router)
services.register(router, as: Router.self)
let leafProvider = LeafProvider() // delete
try services.register(leafProvider) // delete

try services.register(FluentSQLiteProvider())
config.prefer(LeafRenderer.self, for: ViewRenderer.self) // delete
var databases = DatabasesConfig()
try databases.add(database: SQLiteDatabase(storage: .memory), as: .sqlite)
services.register(databases)
var migrations = MigrationConfig()
migrations.add(model: User.self, database: .sqlite)
services.register(migrations)
}

Next delete (move to trash) the Resources/ directory:

projectName/
β”œβ”€β”€ Package.swift
β”œβ”€β”€ Sources/
β”‚ β”œβ”€β”€ App/
β”‚ β”‚ β”œβ”€β”€ Controllers/
β”‚ β”‚ β”‚ └── UserController.swift
β”‚ β”‚ β”œβ”€β”€ Models/
β”‚ β”‚ β”‚ └── User.swift
β”‚ β”‚ β”œβ”€β”€ app.swift
β”‚ β”‚ β”œβ”€β”€ boot.swift
β”‚ β”‚ β”œβ”€β”€ configure.swift
β”‚ β”‚ └── routes.swift
β”‚ └── Run/
β”‚ └── main.swift
β”œβ”€β”€ Tests/
β”œβ”€β”€ Resources/ // delete
β”‚ └── Views/
β”‚ └── userview.leaf
β”œβ”€β”€ Public/
β”œβ”€β”€ Dependencies/
└── Products/

If you now cmd+r or run you should be fine. But if you fire up /users you will get a crash because we still have old code at that route that will try to use a view renderer we don’t have and want anymore. Let’s go ✨!

4. The READ route

We are starting with READ because it is the simplest of all routes. In our Controller/UserController.swift adjust the code to the following:

import Vaporfinal class UserController {  // view with users
func list(_ req: Request) throws -> Future<[User]> {
return User.query(on: req).all()
}
// create a new user
func create(_ req: Request) throws -> Future<Response> {
...
}
}

With our User conforming to Content we are able to return an Array of Users just like that and it will encode into JSON by the power of greysk.. codable ✨

You can cmd+r or run and try the route. It should return an empty array 😊!

5. The CREATE route

In our Controller/UserController.swift adjust the code to the following:

import Vaporfinal class UserController {  // view with users
func list(_ req: Request) throws -> Future<[User]> {
...
}
// create a new user
func create(_ req: Request) throws -> Future<User> {
return try req.content.decode(User.self).flatMap { user in
return user.save(on: req)
}
}

}

Let me for short explain what we do here. We are calling decode on content, pass in our User type with User.self and if the JSON that we get at this route matches with its fields perfectly with the User properties, it will get decoded automagically into a user instance an we can go and save it. The save call will return the saved user which we then return as a response - and that’s it 😊!

NOTE: Firing up a route from the url-section of 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) we would need a tool like Postman or Paw (if you prefer a GUI). But if you want to be nerdy like me you could just use your Terminal with a native command called curl. Let’s curl. πŸ€“

5.1 CURL β€” Overview
First an overview on how the curl command for our POST-request looks like:

curl -H "Content-Type: application/json" -X POST -d '{"username":"zelda"}' http://localhost:8080/users

There are only four parts here. It’s really easier than it looks!
β€’ 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

The reason we have to define what type of data (JSON) we want to send with curl is because curl is sending data form-encoded by default. That is the same encoding used when you submit a form on a website β˜πŸ»πŸ€“. That’s why we need to define the type of data within the header with -H (just as we do) 😊.

Now cmd+r or run your project and check your Xcode Console for the port and let’s fire our first curl request in our terminal to create a user:

curl -H "Content-Type: application/json" -X POST -d '{"username":"zelda"}' http://localhost:8080/users

It should return:

{"id":1,"username":"zelda"}

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. The UPDATE route

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

import Vapor/// Register your application's routes here.
public func routes(_ router: Router) throws {
let userController = UserController()
router.get("users", use: userController.list)
router.post("users", use: userController.create)
router.patch("users", User.parameter, use: userController.update)
}

Wait a second. Where is the id? Well, I am glad you asked Watson. We are going to conform our User to the Parameter protocol so that we are able to define a route like this. It’s nothing fancy, the url that is matching this definition is β€œ/users/23” so basically just the user Id. But instead of defining Int.parameter here and then trying to grab that number inside our controller and then use it to fetch the user with that id from the database. We have the possibility to define User.parameter and let Vapor do the hard work for us!

You will see it in the controller in a second. But first we have to conform our User to Parameter like so:

import FluentSQLite
import Vapor
final class User: SQLiteModel {
var id: Int?
var username: String
init(id: Int? = nil, username: String) {
self.id = id
self.username = username
}
}
extension User: Content {}
extension User: Migration {}
extension User: Parameter {} // added

Now to our Controller/UserController.swift writing the update function:

import Vaporfinal class UserController {  func list(_ req: Request) throws -> Future<[User]> 
...
}
func create(_ req: Request) throws -> Future<User> {
...
}
func update(_ req: Request) throws -> Future<User> {
return try req.parameters.next(User.self).flatMap { user in
return try req.content.decode(User.self).flatMap { newUser in
user.username = newUser.username
return user.save(on: req)
}
}
}

}

So first with req.paremeters.next(User.self) we’ll get our user object fetched from our database for us with the id that was given at the url e.g: β€œusers/1”. And then we decode the received JSON into another User instance. I hope the rest of the function is clear so 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 and create a new user and then remember the id that will be in the returned JSON so we can then use it for our PATCH-request

Create a new user with:

curl -H "Content-Type: application/json" -X POST -d '{"username":"zelda"}' http://localhost:8080/users

It should return:

{"id":1,"username":"zelda"}

And now update (patch) that user :

curl -H "Content-Type: application/json" -X PATCH -d '{"username":"midna"}' http://localhost:8080/users/1

It should return:

{"id":1,"username":"midna"}

It basically looks 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 πŸ˜‰.

7. The DELETE route

Our final chapter! In our routes.swift add:

import Vapor/// Register your application's routes here.
public func routes(_ router: Router) throws {
let userController = UserController()
router.get("users", use: userController.list)
router.post("users", use: userController.create)
router.patch("users", User.parameter, use: userController.update)
router.delete("users", User.parameter, use: userController.delete)
}

I’m not super sure.. but have the feeling you’re already an expert with that kind of route definition.. πŸ€” In our Controllers/UserController.swift add:

import Vaporfinal class UserController {  func list(_ req: Request) throws -> Future<[User]> {
...
}
func create(_ req: Request) throws -> Future<User> {
...
}
func update(_ req: Request) throws -> Future<User> {
...
}
func delete(_ req: Request) throws -> Future<HTTPStatus> {
return try req.parameters.next(User.self).flatMap { user in
return user.delete(on: req)
}.transform(to: .ok)
}

}

Our final cmd+r or run and once again create a user with that curl request your learned earlier. Maybe have a look at /users in your browser to see it returning an array of one user (or more, if you create more). And then og ahead delete that a user by providing his id:

curl -X DELETE http://localhost:8080/users/1

And that’s it! You successfully implemented a CRUD API using JSON πŸŽ‰!

8. Where to go from here

You can find a list of all tutorials with example projects on Github here:
πŸ‘‰πŸ» https://github.com/vaporberlin/vaporschool

I am really happy you read my article! If you have any suggestions or improvements of any kind let me know! I’d love to hear from you! 😊

--

--

Martin Lasek
Martin Lasek

Written by Martin Lasek

I'm an always optimistic, open minded and knowledge seeking fullstack developer passionate about UI/UX and changing things for the better :)

Responses (4)