Chapters

Hide chapters

Server-Side Swift with Vapor

Third Edition · iOS 13 · Swift 5.2 - Vapor 4 Framework · Xcode 11.4

Before You Begin

Section 0: 3 chapters
Show chapters Hide chapters

Section I: Creating a Simple Web API

Section 1: 13 chapters
Show chapters Hide chapters

31. Advanced Fluent
Written by Tim Condon

In the previous sections of this book, you learned how to use Fluent to perform queries against a database. You also learned how to perform CRUD operations on models. In this chapter, you’ll learn about some of Fluent’s more advanced features. You’ll see how to save models with enums and use Fluent’s soft delete and timestamp features. You’ll also learn how to use raw SQL and joins, as well as seeing how to “eager load” relationships.

Getting started

The starter project for this chapter is based on the TIL application from the end of chapter 21. You can either use your code from that project or use the starter project included in the book materials for this chapter. This project relies on a PostgreSQL database running locally.

Clearing the existing database

If you’ve followed along from the previous chapters, you need to delete the existing database. This chapter contains model changes which require either reverting your database or deleting it. In Terminal, type:

docker rm -f postgres

This stops the Docker container named postgres if it’s running and deletes it.

Creating a new database

Create a new database in Docker for the TIL application to use. In Terminal, type:

docker run --name postgres \
  -e POSTGRES_DB=vapor_database \
  -e POSTGRES_USER=vapor_username \
  -e POSTGRES_PASSWORD=vapor_password \
  -p 5432:5432 -d postgres

Here’s what this does:

  • Run a new container named postgres.
  • Specify the database name, username and password through environment variables.
  • Allow applications to connect to the PostgreSQL server on the default port: 5432.
  • Run the server in the background as a daemon.
  • Use the Docker image named postgres for this container. If the image isn’t present on your machine, Docker automatically downloads it.

For more information on how to configure the database in the project, see Chapter 6, “Configuring a Database”.

Soft delete

In Chapter 7, “CRUD Database Operations”, you learned how to delete models from the database. However, while you may want models to appear deleted to users, you might not want to actually delete them. You could also have legal or company requirements which enforce retention of data. Fluent provides soft delete functionality to allow you to do this. Open the TIL app in Xcode and go to User.swift. Look for:

var acronyms: [Acronym]

and add the following definition below:

@Timestamp(key: "deleted_at", on: .delete)
var deletedAt: Date?

This adds a new property for Fluent to store the date you performed a soft delete on the model. You annotate the property with @Timestamp. Fluent checks for this property wrapper when you call delete(on:). If the property exists for the .delete action, Fluent sets the current date on the property and saves the updated model. Otherwise, it deletes the model from the database. That’s all that’s required to implement soft delete in Fluent!

Next, open CreateUser.swift. In prepare(on:), before .unique(on: "username") add:

.field("deleted_at", .datetime)

This adds a field to the migration so Fluent creates the correct column for the new property.

Open UsersController.swift and create a route to use the new functionality. Below loginHandler(_:), add the following:

func deleteHandler(_ req: Request) 
  -> EventLoopFuture<HTTPStatus> {
    User.find(req.parameters.get("userID"), on: req.db)
      .unwrap(or: Abort(.notFound)).flatMap { user in
        user.delete(on: req.db).transform(to: .noContent)
    }
}

This deletes the user passed as a parameter and returns a 204 No Content response. Finally, you need to register the route. Add the following to the end of boot(routes:):

tokenAuthGroup.delete(":userID", use: deleteHandler)

This routes a DELETE request to /api/users/<USER_ID> to deleteHandler(_:). Build and run the Vapor application. In RESTed, using the pre-defined admin user, send a request to http://localhost:8080/api/users/login with the correct HTTP Basic Authentication credentials to get a token. See Chapter 18, “API Authentication, Part 1” for a refresher on how to do this.

Next, create a new request and configure it as follows:

Add three parameters with names and values:

  • username: a username of your choice
  • name: a name of your choice
  • password: a password of your choice

Click Send Request. This creates a user in the application:

Next, send a request to delete the new user. Configure the request as follows:

Click Send Request. You should see a 204 No Content response, indicating you successfully performed a soft delete of the user. Finally, configure a request to get all the users:

Click Send Request . You’ll note that even though you only soft deleted the user, it doesn’t appear in the list of all users:

Restoring Users

Even though the application now allows you to soft delete users, you may want to restore them at a future date. First, add the following below import Vapor at the top of UsersController.swift:

import Fluent

This allows you to use Fluent’s filter functions. Next, create a new route handler below deleteHandler(_:) to restore a user:

func restoreHandler(_ req: Request) 
  throws -> EventLoopFuture<HTTPStatus> {
    // 1
    let userID = 
      try req.parameters.require("userID", as: UUID.self)
    // 2
    return User.query(on: req.db)
      .withDeleted()
      .filter(\.$id == userID)
      .first()
      .unwrap(or: Abort(.notFound))
      .flatMap { user in
        // 3
        user.restore(on: req.db).transform(to: .ok)
    }
}

Here’s what’s going on:

  1. Get the user’s ID as a UUID from the request’s parameters.
  2. Perform a query to find the user with that ID. withDeleted() tells Fluent to include soft-deleted models.
  3. Call restore(on:) on the user to restore that user. Transform the response to 200 OK.

Finally, register the route handler. Add the following to the end of boot(routes:):

tokenAuthGroup.post(":userID", "restore", use: restoreHandler)

This maps a POST request to /api/users/<UUID>/restore to restoreHandler(_:). Build and run the application and open RESTed. Configure a request as follows, using the UUID of the user you deleted above:

Click Send Request. You’ll receive a 200 OK response, indicating you’ve restored the user.

If you no longer have the UUID of the user, you can retrieve it using the following magic in Terminal:

docker exec -it postgres psql -U vapor_username vapor_database
select id from "users" where username = ’<your username>’;
\q

Configure a final request as follows:

Click Send Request. The restored user now appears in the list of users:

Force delete

Now that you can soft delete and restore users, you may want to add the ability to properly delete a user. You use force delete for this. Back in Xcode, still in UsersController.swift, create a new route to do this. Add the following below restoreHandler(_:):

func forceDeleteHandler(_ req: Request) 
  -> EventLoopFuture<HTTPStatus> {
    User.find(req.parameters.get("userID"), on: req.db)
      .unwrap(or: Abort(.notFound))
      .flatMap { user in
        user.delete(force: true, on: req.db)
          .transform(to: .noContent)
  }
}

Your code is similar to deleteHandler(_:). However, this time you call delete(force:on:) on the model. Setting force to true bypasses the soft delete and removes the model from the database.

Finally, register the route handler. Add the following to the end of boot(routes:):

tokenAuthGroup.delete(
  ":userID", 
  "force", 
  use: forceDeleteHandler)

This routes a DELETE request to /api/users/<USER_ID>/force to forceDeleteHandler(_:). Build and run the application and go back to RESTed. Configure a new request as follows:

Click Send Request and you’ll receive a 204 No Content response. Configure a final request as follows:

Click Send Request.

You’ll receive a 404 Not Found error as the model no longer exists in the database to be restored:

Timestamps

Fluent has built-in functionality for timestamps for a model’s creation time and update time. In fact, you used one above to implement soft-delete functionality. If you configure these, Fluent automatically sets and updates the times. To enable this, open Acronym.swift in Xcode. Below var categories: [Category] add two new properties for the dates:

@Timestamp(key: "created_at", on: .create)
var createdAt: Date?

@Timestamp(key: "updated_at", on: .update)
var updatedAt: Date?

Just like soft deletes, Fluent looks for these timestamps when creating and updating models. If they exist, Fluent sets the dates. Now, open CreateAcronym.swift. In prepare(on:), before .create() add the following:

.field("created_at", .datetime)
.field("updated_at", .datetime)

This adds the two new fields to the migration so Fluent creates the columns in the database. That’s all that’s required! Create a new route handler to use the functionality.

Open AcronymsController.swift and add the following below removeCategoriesHandler(_:):

func getMostRecentAcronyms(_ req: Request) 
  -> EventLoopFuture<[Acronym]> {
    Acronym.query(on: req.db)
      .sort(\.$updatedAt, .descending)
      .all()
}

This route returns all acronyms, sorted by updatedAt. The sort uses a descending order to ensure the most recent appear first. For more information on how to use sort(_:), see Chapter 7, “CRUD Database Operations”. Fluent sets createdAt when you create the model. Fluent also sets updatedAt when you create the model and any time you update it. Register this route in boot(routes:) below acronymsRoutes.get(":acronymID", "categories", use: getCategoriesHandler) with the following:

acronymsRoutes.get("mostRecent", use: getMostRecentAcronyms)

This routes a GET request to /api/acronyms/mostRecent to getMostRecentAcronyms(_:). Before you run the application, you must either update or reset the database to add the new fields in for Acronym. For the sake of time, this chapter resets the Docker database. To change the table using a migration, see Chapter 27, “Database/API Versioning & Migration”. In Terminal, run the following commands:

docker rm -f postgres
docker run --name postgres \
  -e POSTGRES_DB=vapor_database \
  -e POSTGRES_USER=vapor_username \
  -e POSTGRES_PASSWORD=vapor_password \
  -p 5432:5432 -d postgres

These commands stop, delete and recreate the PostgreSQL database in Docker, as described at the start of this chapter. Finally, build and run the application and open RESTed. Create a few acronyms, remembering you need to log in first, as described in Chapter 18, “API Authentication, Part 1”.

Hint: You might find it simpler to use the Web interface to add the acronyms by visiting http://localhost:8080 in your browser.

Next, configure a new request in RESTed as follows:

This updates the first acronym created. Add two parameters with names and values:

  • short: the same short as the original acronym, e.g. OMG
  • long: an updated meaning for the acronym, e.g. Oh My Gosh

Click Send Request to update the acronym. Finally, configure a new request in RESTed as follows:

Click Send Request to get the list of all acronyms, sorted by most recently updated. You’ll see the first acronym appears first in the list, since you updated it last:

Enums

A common requirement for database columns is to restrict the values to a pre-defined set. Both FluentPostgreSQL and FluentMySQL support enums for this. To demonstrate this, you’ll add a type to the user to define basic user access levels. In Xcode, create a new file called UserType.swift in Sources/App/Models. Open the new file and add the following:

import Foundation

// 1
enum UserType: String, Codable {
  // 2
  case admin
  case standard
  case restricted
}

Here’s what the new code does:

  1. Create a new String enum type, UserType that conforms to Codable. The type must be a String enum to conform to Codable.
  2. Define three types of user access for use in the Vapor application.

Next, open User.swift and add a new property below var deletedAt: Date? to store the user’s type:

@Enum(key: "userType")
var userType: UserType

This adds a new property for User. You annotate the property with @Enum. This is a special type of Field property wrapper used to store native database enums. Change the initializer to support the new property:

init(
  id: UUID? = nil,
  name: String,
  username: String,
  password: String,
  userType: UserType = .standard
) {
  self.name = name
  self.username = username
  self.password = password
  self.userType = userType
}

This defaults the user type to a newly created user to a standard user. Finally, in CreateAdminUser.swift, change let user = User(...) to the following:

let user = User(
  name: "Admin", 
  username: "admin", 
  password: passwordHash, 
  userType: .admin)

This makes the admin user an admin type. Then, open CreateUser.swift. Replace the body of prepare(on:) with the following:

// 1
database.enum("userType")
  // 2
  .case("admin")
  .case("standard")
  .case("restricted")
  // 3
  .create()
  .flatMap { userType in
    database.schema("users")
      .id()
      .field("name", .string, .required)
      .field("username", .string, .required)
      .field("password", .string, .required)
      .field("deleted_at", .datetime)
      // 4
      .field("userType", userType, .required)
      .unique(on: "username")
      .create()
}

Here’s what the new code does:

  1. Set up a database enum using enum(_:). This is similar to setting up a table using schema(_:_).
  2. Define the different cases for your enum.
  3. Call create() to create the enum in the database. Wait for the create to complete using flatMap(_:). The closure for flatMap(_:) receives the enum type created.
  4. Use the enum type to define a new field in the users table for the new property.

Next, open UsersController.swift to make use of this new property. Replace the function signature of deleteHandler(_:) with the following:

func deleteHandler(_ req: Request) 
  throws -> EventLoopFuture<HTTPStatus> {

This allows you to throw errors in the function body. Next, replace the body of deleteHandler(_:) with the following:

// 1
let requestUser = try req.auth.require(User.self)
// 2
guard requestUser.userType == .admin else {
  throw Abort(.forbidden)
}
// 3
return User.find(req.parameters.get("userID"), on: req.db)
  .unwrap(or: Abort(.notFound))
  .flatMap { user in
    user.delete(on: req.db)
      .transform(to: .noContent)
}

The changes made were:

  1. Get the authenticated user from the request.
  2. Ensure the authenticated user is an admin. This ensures that only admins can delete other users. Otherwise, throw a 403 Forbidden response.
  3. Delete the user specified in the request’s parameters, as before.

Reset the database using the commands from earlier, then build and run the application. Open RESTed and log in as the admin user to get a token. Configure a new request as follows:

Add four parameters with names and values:

  • username: a username of your choice
  • name: a name of your choice
  • password: a password of your choice
  • userType: standard

Click Send Request to create the user. Change the values to create another user to delete and click Send Request. Take a note of the second user’s ID. Log in as the first user you created and configure another request as follows:

Click Send Request, and you’ll receive a 403 Forbidden response:

Change the Authorization header to use the token from the admin user and click Send Request again. This time the request succeeds, and you’ll receive a 204 No Content response:

Note: To be more complete, you should make the same changes to forceDeleteHandler(_:) and restoreHandler(_:). This is left as an exercise for the reader.

Lifecycle hooks

Fluent allows you to hook into various aspects of a model’s lifecycle using model middleware. These work in a similar way to other middleware and allow you to execute code before and after different events. For more information on middleware, see Chapter 29, “Middleware”. Fluent allows you to add middleware for the following events:

  • create: called when Fluent creates a model.
  • update: called when Fluent updates a model.
  • delete: called when Fluent deletes a model.
  • softDelete: called when Fluent soft deletes a model.
  • restore: called when Fluent restores a model.

These hooks allow you to add additional checks to your models, populate or remove fields or add extra steps such as log messages. To demonstrate this, create a new file in Sources/App/Models called UserMiddleware.swift. Open the new file and add the following:

import Fluent
import Vapor

// 1
struct UserMiddleware: ModelMiddleware {
  // 2
  func create(
    model: User, 
    on db: Database, 
    next: AnyModelResponder) -> EventLoopFuture<Void> {
    // 3
    User.query(on: db)
      .filter(\.$username == model.username)
      .count()
      .flatMap { count in
        // 4
        guard count == 0 else {
          let error = 
            Abort(
              .badRequest, 
              reason: "Username already exists")
          return db.eventLoop.future(error: error)
        }
        // 5
        return next.create(model, on: db).map {
          // 6
          let errorMessage: Logger.Message = 
            "Created user with username \(model.username)"
          db.logger.debug(errorMessage)
        }
    }
  }
}

Here’s what the new code does:

  1. Create a new type that conforms to ModelMiddleware.
  2. Implement create(model:on:next:) to perform additional checks before you create a user.
  3. Query the database to get the number of users with the new user’s username.
  4. Ensure there are no users with that username, otherwise return a failed future with an AbortError and reason. This returns a better error message to the client than the database constraint violation message. Returning a failed future cancels the save. You should still use the database constraint to assert that a username is unique in case two users try and register with the same username at the exact same time.
  5. Chain the next responder to allow other middleware to run.
  6. Log a message to the console once the save completes. You can run additional code after Fluent has saved the model here.

It’s useful to validate unique usernames using a ModelMiddleware as you only have to do it in one place. The TIL app contains two places to create users — the API and the website. By using a ModelMiddleware, you don’t need to duplicate the logic to ensure usernames are unique.

Finally, open configure.swift to register the middleware. Below app.migrations.add(CreateAdminUser()) add the following:

app.databases.middleware.use(UserMiddleware(), on: .psql)

This registers UserMiddleware to psql to ensure it runs whenever you create a User.

Build and run the application and log in to get a token, if you don’t already have one. In RESTed, configure a new request as follows:

Add four parameters with names and values:

  • username: admin
  • name: Admin
  • password: password
  • userType: admin

Click Send Request, and you’ll see the error message returned since the admin username already exists:

Eager loading and nested models

If you follow a strict REST API, you should retrieve a model’s children in a separate request. However, this isn’t alway ideal, and you may want the ability to send a single request to get all models with all their children. For example, in the TIL application, you may want a route that returns all categories with all their acronyms. You may even want to return all categories with all their acronyms with all their users. This is commonly referred to as the N+1 problem and Fluent makes this easy with eager loading. Open CategoriesController.swift and add the following at the bottom of the file:

struct AcronymWithUser: Content {
  let id: UUID?
  let short: String
  let long: String
  let user: User.Public
}

struct CategoryWithAcronyms: Content {
  let id: UUID?
  let name: String
  let acronyms: [AcronymWithUser]
}

This defines two new types to use when returning all the categories with their acronyms and the acronyms’ users. Below getAcronymsHandler(_:), add the code to perform the query:

func getAllCategoriesWithAcronymsAndUsers(_ req: Request) 
  -> EventLoopFuture<[CategoryWithAcronyms]> {
    // 1
    Category.query(on: req.db)
      // 2
      .with(\.$acronyms) { acronyms in
        // 3
        acronyms.with(\.$user)
      // 4
      }.all().map { categories in
        // 5
        categories.map { category in
          // 6
          let categoryAcronyms = category.acronyms.map {
            AcronymWithUser(
              id: $0.id, 
              short: $0.short, 
              long: $0.long, 
              user: $0.user.convertToPublic())
          }
          // 7
          return CategoryWithAcronyms(
            id: category.id, 
            name: category.name, 
            acronyms: categoryAcronyms)
        }
      }
}

Here’s what the new route handler does:

  1. Perform a query on Category to get all the categories.
  2. Eager load the categories’ acronyms using with(_:). with(_:) accepts a key path to the relationship to eager load — in this case, $acronyms.
  3. with(_:) also accepts an optional closure allowing you to nest eager loads. This allows you to eager load $user on Acronym at the same time. Fluent works out the queries it needs to perform for you.
  4. Use all() to finish the query and get all the results.
  5. Loop through all the returned categories to convert them to CategoryWithAcronyms.
  6. Convert all the category’s acronyms to AcronymWithUser. When you eager load a model’s relationships, you can access the property directly. You don’t need to go through the property wrapper like previous chapters. Be warned: If you do this without eager loading the relationship, you’ll get a fatal error.
  7. Return the category converted to CategoryWithAcronyms.

Finally, register the route in boot(routes:) below categoriesRoute.get(":categoryID", "acronyms", use: getAcronymsHandler):

categoriesRoute.get(
  "acronyms", 
  use: getAllCategoriesWithAcronymsAndUsers)

The routes a GET request to /api/categories/acronyms to getAllCategoriesWithAcronymsAndUsers(_:). Build and run the application and create some users and acronyms and categories. In RESTed, configure a new request as follows:

Click Send Request and you’ll see all the categories with their acronyms and the acronyms have their users:

Joins

Sometimes, you want to query other tables when retrieving information. For example, you might want to get the user who created the most recent acronym. You could do this with eager loading and Swift. You’d do this by getting all the users and eager load their acronyms. You can then sort the acronyms by their created date to get the most recent and return its user. However, this means loading all users and their acronyms into memory, even if you don’t want them, which is inefficient. Joins allow you to combine columns from one table with columns from another table by specifying the common values. For example, you can combine the acronyms table with the users table using the users’ IDs. You can then sort, or even filter, across the different tables.

Open UsersController.swift and add a route handler below forceDeleteHandler(_:) to get users who have created acronyms recently:

func getUserWithMostRecentAcronym(_ req: Request) 
  -> EventLoopFuture<User.Public> {
    // 1
    User.query(on: req.db)
      // 2
      .join(Acronym.self, on: \Acronym.$user.$id == \User.$id)
      // 3
      .sort(Acronym.self, \Acronym.$createdAt, .descending)
      // 4
      .first()
      .unwrap(or: Abort(.internalServerError))
      .convertToPublic()
}

Here’s what the new code does:

  1. Perform a query on User.
  2. Join User to Acronym by linking the user’s ID to the acronym’s user’s $id value.
  3. Sort on Acronym and sort on the createdAt property to get the most recent acronyms. You can use sort and filters with a join.
  4. Return the first user and return an internal server error if one doesn’t exist. The database should always contain at least one user with the admin user. Note that this returns just User models and not acronyms.

Register the route in boot(routes:) under usersRoute.get(":userID", "acronyms", use: getAcronymsHandler):

usersRoute.get(
  "mostRecentAcronym", 
  use: getUserWithMostRecentAcronym)

This routes a GET request to /api/users/mostRecentAcronym to getUserWithMostRecentAcronym(_:). Build and run the application and launch RESTed. Configure a new request as follows:

Click Send Request and you’ll see the user who created the most recent acronym:

Raw SQL

Whilst Fluent provides tools to allow you to build lots of different behaviors, there are some advanced features it doesn’t offer. Fluent doesn’t support querying different schemas or aggregate functions. In a complex application, you may find that there are scenarios where Fluent doesn’t provide the functionality you need. In these cases, you can use raw SQL queries to interact with the database directly. This allows you to perform any type of query the database supports.

In AcronymsController.swift, add the following add the top of the file below import Fluent:

import SQLKit

This allows you to see the necessary methods for raw queries. Next, below getMostRecentAcronyms(_:), add:

func getAllAcronymsRaw(_ req: Request) 
  throws -> EventLoopFuture<[Acronym]> {
    // 1
    guard let sql = req.db as? SQLDatabase else {
      throw Abort(.internalServerError)
    }
    // 2
    return sql.raw("SELECT * FROM acronyms")
      // 3
      .all(decoding: Acronym.self)
}

Here’s what the code does:

  1. Cast the database on Request to SQLDatabase to allow you to perform raw queries. If the cast fails, return a 500 Internal Server Error.
  2. Use raw(_:) to create a raw query on the database. Note: You must be careful and sanitize any input into your query to avoid injection attacks. raw(_:) supports parameter binding if necessary.
  3. Get all the results and decode the rows to Acronym. Even though this uses a raw query, you still use Codable to convert the data from the database, thereby providing type safety.

Register the new route in boot(routes:) below acronymsRoutes.get("mostRecent", use: getMostRecentAcronyms) with the following:

acronymsRoutes.get("raw", use: getAllAcronymsRaw)

This routes a GET request to /api/acronyms/raw to getAllAcronymsRaw(_:). Build and run your application and head to RESTed. Configure a final request as follows:

Click Send Request and you’ll see all acronyms returned:

Where to go from here?

In this chapter, you learned how to use some of the advanced features Fluent provides to perform complex queries. You also saw how to send raw SQL queries if Fluent can’t do what you need.

With the knowledge of advanced features, you should now be able to build anything with Vapor and Fluent!

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2026 Kodeco Inc.