Tutorial: how to write a CRUD API with Vapor 2

Martin Lasek
Aug 27, 2017 Β· 9 min read
Image for post
Image for post

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.


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/

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 FluentProviderextension 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 Vaporextension Droplet {
func setupRoutes() throws {
}
}

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

import PackageDescriptionlet 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


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;

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 😊


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 Vaporextension 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"}

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 Vaporextension 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"
}

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 Vaporextension 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,
}

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 Vaporextension 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!


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