Tutorial: How to write CRUD using Leaf

Martin Lasek
7 min readMay 5, 2018

--

At the end of this tutorial you will Create, Read, Update and Delete (CRUD) on a User using Leaf! πŸš€

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. View Structure
4. The CREATE view
5. The READ view
6. The UPDATE view
7. The DELETE view
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:

// 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"),
.package(url: "https://github.com/vapor/fluent-sqlite.git", from: "3.0.0-rc")
],
targets: [
.target(name: "App", dependencies: ["Vapor", "Leaf", "FluentSQLite"]),
.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. View Structure

Let’s create an own view for each operation. With userview.leaf within Resources/Views/ we already have kinda the CREATE and READ part. But let’s optimize and change the file for a better and cleaner overview. First step: rename the file to e.g. crud.leaf.

NOTE: Don’t forget to adjust your list function within your controller to use crud instead of userview when calling try req.view().render(…) β˜πŸ»πŸ€“

We will prepare our view to have a nice structure and we will make use of bootstrap classes for that. In Resources/Views/crud.leaf change it to:

<!DOCTYPE html>
<html>
<head>
<title>CRUD</title>
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet">
</head>
<body class="container"> <h1 class="text-center"> CRUD </h1>
<div class="row">

<div class="col-xs-12 col-sm-3">
<h2>Create</h2>
</div>
<div class="col-xs-12 col-sm-3">
<h2>Read</h2>
</div>
<div class="col-xs-12 col-sm-3">
<h2>Update</h2>
</div>
<div class="col-xs-12 col-sm-3">
<h2>Delete</h2>
</div>
</div></body>
</html>

If you now cmd+r or run and fire up the /users route you’ll see nice titles!

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

4. The CREATE view

It’s super simple, we only need a form with an input field and a submit button:

...
<div class="col-xs-12 col-sm-3">
<h2>Create</h2>
<form method="POST" action="/users">
<div class="input-group">
<input type="text" name="username" class="form-control">
<span class="input-group-btn">
<input class="btn btn-success" type="submit" value="create" />
</span>
</div>
</form>

</div>
...

We are adding a form that is sending a POST request to the /users route with whatever we entered into the input field linked to the inputs name username.

This is what will arrive to our backend: username=whateverWeEntered

NOTE: No need to re-run your project when you change things in a leaf-file 😊

5. The READ view

The Read view is in terms of simplicity no different. Just add this:

...
<div class="col-xs-12 col-sm-3">
<h2>Read</h2>
#for(user in userlist) {
<div class="row">
<div class="col-xs-12">
<div class="form-group">
<input type="text" name="username" class="form-control" value="#(user.username)" disabled>
</div>
</div>
</div>
}

</div>
...

Since we are passing an array at the key userlist to our view from within our controller, we are able to simply loop on it. Let’s refresh our site. We can now create a new user and see a list of them!

6. The UPDATE view

Let’s get back to coding. We need a new route, in our routes.swift add this:

import Routing
import Vapor
public func routes(_ router: Router) throws { let userController = UserController()
router.get("users", use: userController.list)
router.post("users", use: userController.create)
router.post("users", User.parameter, "update", use: userController.update)
}

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. We have a convenient way with defining User.parameter that will do all 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<View> 
...
}
func create(_ req: Request) throws -> Future<Response> {
...
}
func update(_ req: Request) throws -> Future<Response> {
return try req.parameters.next(User.self).flatMap { user in
return try req.content.decode(UserForm.self).flatMap { userForm in
user.username = userForm.username
return user.save(on: req).map { _ in
return req.redirect(to: "/users")
}
}
}
}

}
struct UserForm: Content {
var username: String
}

Okay we have a lot going on here let’s break it down.

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/23”.

Now this will return a Future<User> and in order to access a value inside a Future we will always have to use one of the map functions. The question is which one? Let’s recap. When we call map or flatMap it’s always on a Future.

We choose map if the body of the call returns a non-future value.

someFuture.map { return value }

And we call flatMap if the body does return a future value.

someFuture.flatMap { return Future<value> }

So secondly we have req.content.decode(UserForm.self) in which we make use of codable. If we have a struct or class that conforms to content and it has all properties that we receive from a Form (could also be JSON) we can easily say to what we want to decode and that will give us an instance of that object.

Finally we are overriding the username of the user with the username of the userForm, save the user and return a redirect. That’s it. Let’s go and add the corresponding Form to crud.leaf:

...
<div class="col-xs-12 col-sm-3">
<h2>Read</h2>
#for(user in userlist) {
<form method="POST" action="/users/#(user.id)/update">
<div class="input-group form-group">
<input type="text" name="username" class="form-control" value="#(user.username)">
<span class="input-group-btn">
<input class="btn btn-primary" type="submit" value="update" />
</span>
</div>
</form>
}

</div>
...

We want to be able to update every user we have, so we loop on the userlist and create a form with method POST to the route users/#(user.id)/update for each of them. We will have for each user a filled out input field with his username and if we change and submit it, we will immediately see the update. We don’t need to rerun for changes in our view, but we added a new function and route β€” so let’s cmd+r or run our project, refresh our site and try updating a username! I really like how simple it is!

7. The DELETE view

This one is simple too. In our Controllers/UserController.swift add:

final class UserController {  func list(_ req: Request) throws -> Future<View> {
...
}
func create(_ req: Request) throws -> Future<Response> {
...
}
func update(_ req: Request) throws -> Future<Response> {
...
}
func delete(_ req: Request) throws -> Future<Response> {
return try req.parameters.next(User.self).flatMap { user in
return user.delete(on: req).map { _ in
return req.redirect(to: "/users")
}
}
}

}

I think (hope 🀞🏻) this code is self explaining, too. Let’s define our last route:

import Routing
import Vapor
public func routes(_ router: Router) throws {
let userController = UserController()
router.get("users", use: userController.list)
router.post("users", use: userController.create)
router.post("users", User.parameter, "update", use: userController.update)
router.post("users", User.parameter, "delete", use: userController.delete)
}

And finally in our crud.leaf the laaast piece of code:

<div class="col-xs-12 col-sm-3">
<h2>Delete</h2>
#for(user in userlist) {
<form method="POST" action="users/#(user.id)/delete">
<div class="form-group input-group">
<input type="text" name="username" class="form-control" value="#(user.username)" disabled>
<span class="input-group-btn">
<input class="btn btn-danger" type="submit" value="delete" />
</span>
</div>
</form>
}

</div>

Our final cmd+r or run and refresh of our site and that’s it! You successfully implemented a CRUD API with Leaf πŸŽ‰!

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 (2)