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 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 {
}
}
3. Add dependencies
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 <-- ADDextension 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 PostgreSQLProviderextension 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 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"}
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 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"
}
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 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,
}
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 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!