Tutorial: how to write a CRUD API with VaporΒ 2

At the end of this tutorial you will have an API with Create, Read, Update and Delete (CRUD) on a User talking JSON to you! πŸš€


You can find the result of this tutorial on github:Β here.

1. Creating a new VaporΒ Project

Note: to do this tutorial you will need to have swift 3, vapor-toolbox and postgresql installed.

We will clone the api-template that vapor provides:

vapor new test-example

Go inside your directory, generate a new Xcode project and open it:

cd test-example/
vapor xcode -y

You should have a project structure like this:

test-example/
β”œβ”€β”€ Package.swift
β”œβ”€β”€ Sources/
β”‚ β”œβ”€β”€ App/
β”‚ β”‚ β”œβ”€β”€ Config+Setup.swift
β”‚ β”‚ β”œβ”€β”€ Droplet+Setup.swift
β”‚ β”‚ β”œβ”€β”€ Routes.swift
β”‚ β”‚ β”œβ”€β”€ Controllers/
β”‚ β”‚ β”‚ └── PostController.swift
β”‚ β”‚ └── Models/
β”‚ β”‚ └── Post.swift
β”‚ └── Run/
β”œβ”€β”€ Tests/
β”œβ”€β”€ Config/
β”œβ”€β”€ Public/
β”œβ”€β”€ Dependencies/
└── Products/

2. Delete (move to trash) files andΒ code

I like to start from scratch so we know exactly which files and implementations are needed for whatπŸ‘ŒπŸ»

test-example/
β”œβ”€β”€ Package.swift
β”œβ”€β”€ Sources/
β”‚ β”œβ”€β”€ App/
β”‚ β”‚ β”œβ”€β”€ Config+Setup.swift
β”‚ β”‚ β”œβ”€β”€ Droplet+Setup.swift
β”‚ β”‚ β”œβ”€β”€ Routes.swift
β”‚ β”‚ β”œβ”€β”€ Controllers/ <-- DELETE
β”‚ β”‚ β”‚ └── PostController.swift <-- DELETE
β”‚ β”‚ └── Models/
β”‚ β”‚ └── Post.swift <-- DELETE
β”‚ └── Run/
β”œβ”€β”€ Tests/
β”œβ”€β”€ Config/
β”œβ”€β”€ Public/
β”œβ”€β”€ Dependencies/
└── Products/

Inside Sources/App/Config+Setup.swift delete the following line:

import FluentProvider
extension Config {
  public func setup() throws {
// allow fuzzy conversions for these types
// (add your own types here)
Node.fuzzy = [Row.self, JSON.self, Node.self]
    try setupProviders()
try setupPreparations()
}
  /// Configure providers
private func setupProviders() throws {
try addProvider(FluentProvider.Provider.self)
}
  /// Add all models that should have their
/// schemas prepared before the app boots
private func setupPreparations() throws {
preparations.append(Post.self) <-- DELETE
}
}

Inside Sources/App/Routes.swift delete everything so it looks like this:

import Vapor
extension Droplet {
func setupRoutes() throws {
}
}

3. Add dependencies

In Package.swift add the following dependency (postgresql):

import PackageDescription
let package = Package(
name: "test-example",
targets: [
Target(name: "App"),
Target(name: "Run", dependencies: ["App"]),
],
dependencies: [
.Package(url: "https://github.com/vapor/vapor.git", majorVersion: 2),
.Package(url: "https://github.com/vapor/fluent-provider.git", majorVersion: 1), <-- DON'T FORGET THIS COMMA ;)
.Package(url: "https://github.com/vapor/postgresql-provider.git", majorVersion: 2)

],
exclude: [
"Config",
"Database",
"Localization",
"Public",
"Resources",
]
)

Now fetch the new dependency in your terminal being in your test-example/ directory, recreate your Xcode project and re-open it:

vapor fetch
vapor xcode -y

In Sources/App/Config+Setup.swift add the PostgreSQLProvider:

import FluentProvider
import PostgreSQLProvider <-- ADD
extension Config {
  public func setup() throws {
// allow fuzzy conversions for these types
// (add your own types here)
Node.fuzzy = [Row.self, JSON.self, Node.self]
    try setupProviders()
try setupPreparations()
}
  /// Configure providers
private func setupProviders() throws {
try addProvider(FluentProvider.Provider.self)
try addProvider(PostgreSQLProvider.Provider.self) <-- ADD
}
  /// Add all models that should have their
/// schemas prepared before the app boots
private func setupPreparations() throws {
}
}
Let’s check if everything went fine so far, make sure you’ve selected My Mac and hit run β–Ί and see if 127.0.0.1:8080 in your browser returns a blank page

4. Configure and createΒ database

Inside Config/ create a new folder called secrets and within the secrets/ directory create a file called postgresql.json:

test-example/
β”œβ”€β”€ Package.swift
β”œβ”€β”€ Sources/
β”œβ”€β”€ Tests/
β”œβ”€β”€ Config/
β”‚ β”œβ”€β”€ app.json
β”‚ β”œβ”€β”€ crypto.json
β”‚ β”œβ”€β”€ droplet.json
β”‚ β”œβ”€β”€ fluent.json
β”‚ β”œβ”€β”€ secrets/ <-- CREATE
β”‚ β”‚ └── postgresql.json <-- CREATE
β”‚ └── server.json
β”œβ”€β”€ Public/
β”œβ”€β”€ Dependencies/
└── Products/

Inside postgresql.json write:

{
"hostname": "127.0.0.1",
"port": 5432,
"user": "martinlasek",
"password": "",
"database": "testexample"
}
Note: you will have to replace the user martinlasek by your username

Inside Config/fluent.json set postgresql as your driver

{
"//": "The underlying database technology to use.",
"//": "memory: SQLite in-memory DB.",
"//": "sqlite: Persisted SQLite DB (configure with sqlite.json)",
"//": "Other drivers are available through Vapor providers",
"//": "https://github.com/search?q=topic:vapor-provider+topic:database",
"driver": "postgresql", <-- change from memory to postgresql
  ...
}

Create the database testexample by typing in your terminal the following:

createdb testexample;

5. Create a UserΒ model

Inside Sources/App/Models/ create a file called User.swift, regenerate and open your Xcode project:

touch Sources/App/Models/User.swift
vapor xcode -y
Since there is noΒ .swift-file in your Models/ directory, Xcode won’t display this directory. Therefor you can’t create a file within Models/ through Xcode πŸ˜…

Inside Sources/App/Models/User.swift paste the following code:

import Vapor
import FluentProvider
import HTTP

final class User: Model {
let storage = Storage()
var username: String
var age: Int

  init(username: String, age: Int) {
self.username = username
self.age = age
}

  // initiate user with database data
init(row: Row) throws {
username = try row.get("username")
age = try row.get("age")
}

  func makeRow() throws -> Row {
var row = Row()
try row.set("username", username)
try row.set("age", age)
return row
}
}

/// MARK: Fluent Preparation
extension User: Preparation {

  // prepares a table in the database
static func prepare(_ database: Database) throws {
try database.create(self) { builder in
builder.id()
builder.string("username")
builder.int("age")
}
}

  // deletes the table from the database
static func revert(_ database: Database) throws {
try database.delete(self)
}
}

/// MARK: JSON
extension User: JSONConvertible {

  // let you initiate user with json
convenience init(json: JSON) throws {
self.init(
username: try json.get("username"),
age: try json.get("age")
)
}

  // create json out of user instance
func makeJSON() throws -> JSON {
var json = JSON()
try json.set(User.idKey, id)
try json.set("username", username)
try json.set("age", age)
return json
}
}

Let your application know about your model by adding it inside Sources/App/Config+Setup.swift

import FluentProvider
import PostgreSQLProvider
extension Config {
public func setup() throws {
// allow fuzzy conversions for these types
// (add your own types here)
Node.fuzzy = [Row.self, JSON.self, Node.self]
    try setupProviders()
try setupPreparations()
}
  /// Configure providers
private func setupProviders() throws {
try addProvider(FluentProvider.Provider.self)
try addProvider(PostgreSQLProvider.Provider.self)
}
  /// Add all models that should have their
/// schemas prepared before the app boots
private func setupPreparations() throws {
preparations.append(User.self) <-- ADD
}
}

Now again let’s run β–Ί the project, it will prepare your database and create a user table. Your Xcode console will show you something like:

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 <-- this is what we look for
No command supplied, defaulting to serve…
Starting server on 0.0.0.0:8080
If Xcode does not let you run your project, just execute vapor xcode -y in your terminal and try running again 😊

6. Implement: Create

In your Sources/App/Routes.swift implement a new route that will expect a JSON-request, initiate a user out of it and persist him to the database:

import Vapor
extension Droplet {
func setupRoutes() throws {
    /// CREATE user
/// http method: post
post("user") { req in
      // check request constains json
guard let json = req.json else {
throw Abort(.badRequest, reason: "no json provided")
}
      let user: User
      // try to initialize user with json
do {
user = try User(json: json)
}
catch {
throw Abort(.badRequest, reason: "incorrect json")
}
      // save user
try user.save()
      // return user
return try user.makeJSON()
}

}
}

If you now run β–Ί your app and POST to 127.0.0.1:8080/user with a valid JSON:

{
"username": "Tom Cruise",
"age": 23
}

it will create a user, save it to the database and return the created user in JSON-format back to you. You can use Postman or if you talk nerdy πŸ€“ just use your terminal typing:

curl -H "Content-Type: application/json" -X POST -d '{"username":"Tom Cruise", "age": 23}' http://127.0.0.1:8080/user

Either way you will get a response looking like this:

{"id": 1, "age": 23, "username": "Tom Cruise"}

7. Implement: Read

In your Sources/App/Routes.swift implement a new route that will expect an id and return you the user with this id in JSON-format:

import Vapor
extension Droplet {
func setupRoutes() throws {
    /// CREATE user
/// http method: post
post("user") { req in
...
}
    /// READ user by given id
/// http method: get
get("user", Int.parameter) { req in
      // get id from url
let userId = try req.parameters.next(Int.self)
      // find user with given id
guard let user = try User.find(userId) else {
throw Abort(.badRequest, reason: "user with id \(userId) does not exist")
}
      // return user as json
return try user.makeJSON()
}
}
}

If you now run β–Ί your app, since it is a GET route, you can fire up 127.0.0.1:8080/user/1 either in your browser or if you talk nerdy πŸ€“ just use your terminal typing:

curl http://127.0.0.1:8080/user/1

it should return you a JSON of the user with the id used in the url looking like:

{
"id": 1,
"age": 23,
"username": "Tom Cruise"
}

8. Implement: Update

In your Sources/App/Routes.swift implement a new route that will expect a JSON-Request with an id for the user to update and return you the updated user in JSON-format:

import Vapor
extension Droplet {
func setupRoutes() throws {
    /// CREATE user
/// http method: post
post("user") { req in
...
}
    /// READ user by given id
/// http method: get
get("user", Int.parameter) { req in
...
}

/// UPDATE user fully
/// http method: put
put("user", Int.parameter) { req in
      // get userId from url
let userId = try req.parameters.next(Int.self)
      // find user by given id
guard let user = try User.find(userId) else {
throw Abort(.badRequest, reason: "user with given id: \(userId) could not be found")
}
      // check username is provided by json
guard let username = req.data["username"]?.string else {
throw Abort(.badRequest, reason: "no username provided")
}
      // check age is provided by json
guard let age = req.data["age"]?.int else {
throw Abort(.badRequest, reason: "no age provided")
}
      // set new values to found user
user.username = username
user.age = age
      // save user with new values
try user.save()
      // return user as json
return try user.makeJSON()
}

}
}

If you now run β–Ί your app and PUT to 127.0.0.1:8080/user/1 with a valid JSON:

{
"username": "Link",
"age": 41
}

it will search for the user with given id in the url, update his properties, save him back to the database and also return him back to you in JSON-format. You can use Postman or if you talk nerdy πŸ€“ just use your terminal typing:

curl -H "Content-Type: application/json" -X PUT -d '{"username":"Yamato","age": 41}' http://127.0.0.1:8080/user/1

it should return you a JSON of the updated user looking like:

{
"id": 1,
"username": "Yamato"
"age": 41,
}

9. Implement: Delete

In your Sources/App/Routes.swift implement a new route that will expect an id and return you the a JSON with success message

import Vapor
extension Droplet {
func setupRoutes() throws {
    /// CREATE user
/// http method: post
post("user") { req in
...
}
    /// READ user by given id
/// http method: get
get("user", Int.parameter) { req in
...
}

/// UPDATE user fully
/// http method: put
put("user", Int.parameter) { req in
...
}
    /// DELETE user by id
/// http method: delete
delete("user", Int.parameter) { req in
      // get user id from url
let userId = try req.parameters.next(Int.self)
      // find user with given id
guard let user = try User.find(userId) else {
throw Abort(.badRequest, reason: "user with id \(userId) does not exist")
}
      // delete user
try user.delete()
      return try JSON(node: ["type": "success", "message": "user with id \(userId) were successfully deleted"])
}

}
}

If you now run β–Ί your app and DELETE to 127.0.0.1:8080/user/1 it will search for the user with given id in the url, delete him and return a JSON with a success message including the user id. You can use Postman or if you talk nerdy πŸ€“ just use your terminal typing:

curl -X DELETE http://127.0.0.1:8080/user/1

It should return you a JSON looking like:

{
"type": "success",
"message": "user with id 1 were successfully deleted"
}

Congrats! You successfully implemented an API with CRUD on a User πŸŽ‰Β !!


Where to go from here? Learn how to test you routesΒ here!

Thank you a lot for reading! If you have any suggestions or improvements let me know! I would love to hear from you! 😊