Tutorial: How to write Models using Fluent

This tutorial will show you how to implement a simple user model, store him to the database, get him back from the database and pass him to the view. 🍃

You can find the result of this tutorial on github here

This tutorial is a natural follow-up of How to use Leaf. 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. Configure your project to use SQLite database
4. Create your first model
5. Implement a GET route to list all users
6. Explaining Async
7. What the F.. uture
8. Create a view
9. Implement a POST route to store a user


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-leaf-template

2. Generate Xcode project

Before we generate an Xcode project we would have to add a database provider. There are each for every database. But for the sake of getting warm with the ORM Fluent we will use an in memory database. Therefor we add Fluent-SQLite as a dependency within our Package.swift:

// swift-tools-version:4.0
import PackageDescription
let package = Package(
name: "projectName", // changed
dependencies: [
.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") // added
],
targets: [
.target(name: "App", dependencies: ["Vapor", "Leaf", "FluentSQLite"]), // added
.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, generating the Xcode project and opening it for you. When done you should have a project structure like this:

projectName/
├── Package.swift
├── Sources/
│ ├── App/
│ │ ├── boot.swift
│ │ ├── configure.swift
│ │ └── routes.swift
│ └── Run/
│ └── main.swift
├── Tests/
├── Resources/
├── Config/
├── Public/
├── Dependencies/
└── Products/
If you see an error including “CNIOOpenSSL” when running — you’re missing a dependency. Just run brew upgrade vapor and re-generate the project ✌🏻😊

3. Configure your project to use a SQLite database

Our first step is to add the FluentSQLiteProvider within our configure.swift:

import Vapor
import Leaf
import FluentSQLite // added
public func configure(
_ config: inout Config,
_ env: inout Environment,
_ services: inout Services
) throws {
  // Register routes to the router
let router = EngineRouter.default()
try routes(router)
services.register(router, as: Router.self)
  let myService = EngineServerConfig.default(port: 8002)
services.register(myService)
  try services.register(LeafProvider())
try services.register(FluentSQLiteProvider()) // added
  config.prefer(LeafRenderer.self, for: TemplateRenderer.self)
}

Next we will initiate a database service, add a SQLiteDatabase to it and register that database service:

import Vapor
import Leaf
import FluentSQLite
public func configure(
_ config: inout Config,
_ env: inout Environment,
_ services: inout Services
) throws {
  // Register routes to the router
let router = EngineRouter.default()
try routes(router)
services.register(router, as: Router.self)
  let myService = EngineServerConfig.default(port: 8002)
services.register(myService)
  try services.register(LeafProvider())
try services.register(FluentSQLiteProvider())
  config.prefer(LeafRenderer.self, for: TemplateRenderer.self)
  var databases = DatabaseConfig()
try databases.add(database: SQLiteDatabase(storage: .memory), as: .sqlite)
services.register(databases)
}

Finally we initiate and register a migration service that we will use later in order to introduce our Model to our database. For now add the following:

import Vapor
import Leaf
import FluentSQLite
public func configure(
_ config: inout Config,
_ env: inout Environment,
_ services: inout Services
) throws {
  // Register routes to the router
let router = EngineRouter.default()
try routes(router)
services.register(router, as: Router.self)
  let myService = EngineServerConfig.default(port: 8002)
services.register(myService)
  try services.register(LeafProvider())
try services.register(FluentSQLiteProvider())
  config.prefer(LeafRenderer.self, for: TemplateRenderer.self)
  var databases = DatabaseConfig()
try databases.add(database: SQLiteDatabase(storage: .memory), as: .sqlite)
services.register(databases)
  var migrations = MigrationConfig()
services.register(migrations)
}

4. Create your first model

Create a directory within Sources/App/ and name it Models/ and within that new directory create a new swift file called User.swift 😊

NOTE: I used the terminal executing mkdir Sources/App/Models/ and touch Sources/App/Models/User.swift

You may have to re-generate your Xcode project with vapor xcode -y in order to let Xcode see your new directory.

In Models/User.swift include the following code:

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 {}

I kept it super simple so we can understand what is going on here. By conforming to SQLiteModel we have to define an optional variable named id that is of type int. It is optional simply because if we initiate a new user in order to store him to the database it’s not up to us to give him an id at that point. He will get an id assigned after storing him to the database.

The conformance to Content makes it possible so our User can convert into for example JSON using Codable if we would return him in a route. Or so he can convert into TemplateData which is used within a Leaf view. And due to Codable that happens automagically. Conforming to Migration is needed so Fluent can use Codable to create the best possible database table schema and also so we are able to add it to our migration service in our configure.swift:

import Vapor
import Leaf
import FluentSQLite
public func configure(
_ config: inout Config,
_ env: inout Environment,
_ services: inout Services
) throws {
  // Register routes to the router
let router = EngineRouter.default()
try routes(router)
services.register(router, as: Router.self)
  let myService = EngineServerConfig.default(port: 8002)
services.register(myService)
  try services.register(LeafProvider())
try services.register(FluentSQLiteProvider())
  config.prefer(LeafRenderer.self, for: TemplateRenderer.self)
  var databases = DatabaseConfig()
try databases.add(database: SQLiteDatabase(storage: .memory), as: .sqlite)
services.register(databases)
  var migrations = MigrationConfig()
migrations.add(model: User.self, database: .sqlite)
services.register(migrations)
}

If you now cmd+r or run everything should be just fine. 😊

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

5. Implement a GET route to list all users

Yup we don’t have any users in our database yet but we will create and store users using a form we will implement our own. So for now go to routes.swift and delete everything in that file so it ends up looking like this:

import Routing
import Vapor
import Leaf
public func routes(_ router: Router) throws {
// nothing in here
}

Define a get route at the url users fetching all users from the database and pass them into a view:

import Routing
import Vapor
import Leaf
public func routes(_ router: Router) throws {

router.get("users") { req -> Future<View> in
let allUsers = User.query(on: req).all()
    return allUsers.flatMap(to: View.self) { users in
let data = ["userlist": users]
return try req.view().render("userview", data)
}
}

}

Wow. There’s a lot going on here. But no worries it’s way simpler than it looks and you’ll feel great once you read further and understand this black magic ✨

VAPOR 3 is all about Futures. And it comes from its nature being Async now.


6. Explaining Async

Let’s have a life example. In Vapor 2 if a boy was told by his girlfriend to buy her a soft ice and a donut. He would go to the ice wagon, order ice and wait until it’s ready. Then he would continue and go to a donut shop, buy one and go back to his his girlfriend with both.

With Vapor 3 that boy would go to the ice wagon, order ice and in the time the ice is made he would go to the donut shop and buy a donut. He comes back to the ice wagon when the ice is ready, gets it and goes back to his girlfriend with both.

Vapor 2: Boy was blocked by the ice order to finish until he can proceed.
Vapor 3: Boy works non-blocking and uses his waiting time for other tasks.

7. What the F.. uture

Let’s understand what is happening and why. So the first line is quite obvious we’re just creating our leaf renderer. The second line is more interesting. We are using our User class to query the database. And you can read it like executing that query on the back of our request. Think of the request as being the boy. The one who does the work for us. The worker.

Okay so we are storing the expected result in our allUsers variable. Now I purposely said expected because we don’t have an array of users in our variable. Instead we have a future of an array of users: Future<[Users]>. And honestly. That’s it. And what I mean by that is: there’s is nothing fancy or special to it. That’s simply it. It’s just “wrapped” by a Future. The only thing that we need to care about is how in mothers name do we get our data out of the future so we can continue coding the way we are used to code 😊

That’s where map or flatMap comes into play. With both we can access our array of users inside the Future like:

allUsers.flatMap(to: [User].self) { userlist in
// return something you want to map to
}
/// and
allUsers.map(to: [User].self) { userlist in
// return something you want to map to
}

If you call one of the map functions you need to specify upfront to what type you are mapping. It is implemented that way because otherwise the compiler just don’t know what type you are mapping to. Again, nothing special 😊.

So when to use what you may ask? There is a very simple rule. Since you need to return something in each map function, that something is what tells you whether you use flatMap or map. The rule is: If that something is a Future you use flatMap and if it is “normal” data thus not a Future you use map.

So in our route we want to access the array of users in order to pass it to our view. So we need one of both map functions. And since the render() function returns a Future<View> we have learned: if we return a Future use flatMap.

And that’s all we do here. No worries if it feels weird and new and not so intuitive. And you don’t feel like you would know when to use what and how. That’s what I am here for (hopefully) 😊. Follow along the tutorials and ask me or the community in slack all kind of questions and believe me it will click!


8. Create a view

Within Resources/Views/ delete all files you find in there (welcome.leaf and whoami.leaf) and create a new file named userview.leaf and add:

<!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">
<h1 class="mt-3"> Userlist </h1>
    <form method="POST" action="/users">
<div class="input-group">
<input type="text" name="username" class="form-control">
<div class="input-group-append">
<button class="btn btn-outline-secondary" type="submit">
Button
</button>
</div>
</div>
</form>
    #for(user in userlist) {
<p class="mb-0">
#(user.username)
</p>
}

</body>
</html>

I have marked the interesting things here. With the <link> tag I just add bootstrap which is a css framework that makes our view look a bit nicer.

With the <form> tag we define what shall happen when we submit the form, which is firing at /user with the method post. We will implement that post route in second.

The <input type=”text” name=”username”> is our input field to write a new username into and here the name=”username” is super important because “username” is the key where our text will be connected to. You’ll understand what I mean when we write our post route.

The #for() loop is a leaf specific tag where we iterate over the userlist we passed earlier to the view as [“userlist”: …] and with #(user.username) we can access our objects just like in swift.

If you now cmd+r or run the project and fire up your site at /user you will see the header, input field and a button. And that’s perfectly fine. The list of user will appear as soon as we have created some. Let’s do it 🚀!

NOTE: watch your Xcode console to see where your project is running, for me it’s 127.0.0.1:8003 — you can change the port in configure.swift

9. Implement a POST route to store a user

In our routes.swift add the following code:

import Routing
import Vapor
import Leaf
public func routes(_ router: Router) throws {
  router.get("users") { req -> Future<View> in
...
}
  router.post("users") { req -> Future<Response> in
let user = try req.content.decode(User.self)
    return user.save(on: req).map(to: Response.self) { _ in
return req.redirect(to: "users")
}
}

}

When we submit our form in the view by hitting the submit button, it will send the input-field data form-url-encoded to the /user route as a post like:

username=MartinLasek

Since our User model consists of only one property username and conforms to the protocol Content we are able to to decode the content that is sent by the form into an instance of our user. You can try what happens if you add another property to our User class like age of type Int, re-run the project and try submitting the form again. It will tell you that it couldn’t find an Int at the path age. Because our form doesn’t send age=23 alongside the username.

However since save returns a Future of an user instance, we will have to use a map function in order to access it. And since we want to return a redirect to our view after we have successfully saved our user and we see that redirect is not a Future, we use map and not flatMap here 😊

To access a future value you need either map or flatMap. If your return value inside a map function is not a future you use map otherwise flatMap.

If you now cmd+r or run your project and fire up your site in your browser you will be able to create new users and see a growing list when doing so 🙌🏻

NOTE: Since we use an in memory database all data will be gone after re-run 😉

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

One clap, two clap, three clap, forty?

By clapping more or less, you can signal to us which stories really stand out.