Tutorial: How to write CRUD using Leaf

Martin Lasek
7 min readOct 21, 2017

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 😊

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 --branch=vapor-2

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.0import PackageDescriptionlet package = Package(
name: "projectName", // changed
products: [
.library(name: "App", targets: ["App"]),
.executable(name: "Run", targets: ["Run"])
],
dependencies: [
.package(url: "https://github.com/vapor/vapor.git", .upToNextMajor(from: "2.1.0")),
.package(url: "https://github.com/vapor/leaf-provider.git", .upToNextMajor(from: "1.1.0")),
.package(url: "https://github.com/vapor/fluent-provider.git", .upToNextMajor(from: "1.3.0"))
],
targets: [
.target(name: "App", dependencies: ["Vapor", "LeafProvider", "FluentProvider"],
exclude: [
"Config",
"Public",
"Resources",
]),
.target(name: "Run", dependencies: ["App"]),
.testTarget(name: "AppTests", dependencies: ["App", "Testing"])
]
)

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
β”‚ β”‚ β”œβ”€β”€ Routes/
β”‚ β”‚ β”‚ └── Routes.swift
β”‚ β”‚ └── Setup/
β”‚ β”‚ β”œβ”€β”€ Config+Setup.swift
β”‚ β”‚ └── Droplet+Setup.swift
β”‚ └── Run/
β”œβ”€β”€ Tests/
β”œβ”€β”€ Config/
β”œβ”€β”€ 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 drop.view.make(…) β˜πŸ»πŸ€“

We will structure our view first and make use of bootstrap classes for a nicer look since we load the framework into our view if you take a closer look at the <head> section πŸ˜‰. In Resources/Views/crud.leaf remove all the old code:

<!DOCTYPE html>
<html>
<head>
<title>Model</title>
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet">
</head>
<body class="container"> // delete my inside </body>
</html>

And add the following code for a nice overview:

<!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 /user 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 actually pretty simple we had this before when we had userview.leaf:

<div class="col-xs-12 col-sm-3">
<h2>Create</h2>
<form method="POST" action="/user">
<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 /user route with the data entered into the input of type text linked to the inputs name username.

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

5. The READ view

The same goes for this, we had this before, just add the following under Read:

<div class="col-xs-12 col-sm-3">
<h2>Read</h2>
#loop(userlist, "user") {
<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 a userlist to our view in our controller, we can loop on it. If we now refresh our site we can easily create new user and see a list of them!

6. The UPDATE view

Now we will have something new. A new route and a new form. In our Controllers/UserController.swift add this new function:

final class UserController {
let drop: Droplet
init(drop: Droplet) {
self.drop = drop
}
func list(_ req: Request) throws -> ResponseRepresentable {
...
}
func create(_ req: Request) throws -> ResponseRepresentable {
...
}
func update(_ req: Request) throws -> ResponseRepresentable {
guard let userId = req.parameters["id"]?.int else {
return Response(status: .badRequest)
}
guard let username = req.data["username"]?.string else {
return Response(status: .badRequest)
}
guard let user = try User.find(userId) else {
return Response(status: .badRequest)
}
user.username = username
try user.save()
return Response(redirect: "/user")
}

}

With req.parameters[β€œid”]?.int we get the value at the position :id in our route (we will define the route in our Routes/Routes.swift with that :id thingy in a second) and try convert it into an int. We need the id to find the related user that we want to update. We assign the new username to the found user and save him back to the database. I think no code comments are needed here, this is straight forward right 😊 ?

NOTE: User.find() is a shorthand by Fluent to easily and fast find a record by id.

Now to our Routes/Routes.swift, add the following route definition:

import Vapor
extension Droplet {
func setupRoutes() throws {
let userController = UserController(drop: self)
get("user", handler: userController.list)
post("user", handler: userController.create)
post("user", ":id", "update", handler: userController.update)
}
}

This will define a route of look: /user/:id/update where at the place of :id we will provide a number. And it must be a number because otherwise the conversion into int (just as we defined a second ago πŸ˜‰ ) wont work and we get a BadRequest returned.

Now back to our crud.leaf file, let’s add the following:

<div class="col-xs-12 col-sm-3">
<h2>Update</h2>
#loop(userlist, "user") {
<form method="POST" action="/user/#(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 user/#(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 {
let drop: Droplet
init(drop: Droplet) {
self.drop = drop
}
func list(_ req: Request) throws -> ResponseRepresentable {
...
}
func create(_ req: Request) throws -> ResponseRepresentable {
...
}
func update(_ req: Request) throws -> ResponseRepresentable {
...
}
func delete(_ req: Request) throws -> ResponseRepresentable {
guard let userId = req.parameters["id"]?.int else {
return Response(status: .badRequest)
}
guard let user = try User.find(userId) else {
return Response(status: .badRequest)
}
try user.delete()
return Response(redirect: "/user")
}

}

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

import Vapor
extension Droplet {
func setupRoutes() throws {
let userController = UserController(drop: self)
get("user", handler: userController.list)
post("user", handler: userController.create)
post("user", ":id", "update", handler: userController.update)
post("user", ":id", "delete", handler: userController.delete)
}
}

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

<div class="col-xs-12 col-sm-3">
<h2>Delete</h2>
#loop(userlist, "user") {
<form method="POST" action="user/#(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 πŸŽ‰ !

NOTE: You may have noticed something. We did two little things that are not common or even meant to be like that in real world application. We used the HTTP method POST for updating and deleting a user. You would normally use the HTTP methods PATCH (update) and DELETE (delete). But these methods are not available for the form-tag (<form>) and we would had to write javascript to fire those requests. But I wanted to keep this tutorial simple and approachable. Don’t worry too much it still works super fine like it is right now! It’s just that by definition we are β€œmisusing” these methods for a purpose they weren’t designed for 😜. I created another tutorial where we would write a CRUD API using JSON and use the methods accordingly to their purpose β€” in case you’re interested 😊

Thank you a lot for reading! If you have any questions or improvements β€” write a comment! I would love to hear from you! 😊

--

--

Martin Lasek

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