Chapters

Hide chapters

Server-Side Swift with Vapor

Third Edition - Early Acess 1 · 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

30. Advanced Fluent
Written by Tim Condon

Note: This update is an early-access release. This chapter has not yet been updated to Vapor 4.

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 return nested models.

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 stop postgres
docker rm postgres

This stops the Docker container named postgres 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 \
  -e POSTGRES_USER=vapor -e POSTGRES_PASSWORD=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 Postgres 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. Below var profilePicture: String?, add the following:

var deletedAt: Date?

This adds a new property for Fluent to store the date you performed a soft delete on the model. Next, expand extension User: PostgreSQLUUIDModel {} and add the following:

static let deletedAtKey: TimestampKey? = \.deletedAt

This provides the key path that Fluent checks when you call delete(on:). If the key path exists, 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!

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

func deleteHandler(_ req: Request)
  throws -> Future<HTTPStatus> {
    return try req.parameters
      .next(User.self)
      .delete(on: req)
      .transform(to: .noContent)
}

This deletes the user passed as a parameter and returns a 204 No Content response. Register the route in boot(router:) below tokenAuthGroup.post(User.self, use: createHandler):

tokenAuthGroup.delete(User.parameter, 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 four parameters with names and values:

  • username: a username of your choice
  • name: a name of your choice
  • email: an email address 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 Crypto 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 -> Future<HTTPStatus> {
    // 1
    let userID = try req.parameters.next(UUID.self)
    // 2
    return User.query(on: req, withSoftDeleted: true)
      .filter(\.id == userID)
      .first().flatMap(to: HTTPStatus.self) { user in
        // 3
        guard let user = user else {
          throw Abort(.notFound)
        }
        // 4
        return user.restore(on: req).transform(to: .ok)
    }
}

Here’s what’s going on:

  1. Get the user’s ID as a UUID from the request’s parameters. Using the user as a parameter won’t work as you’ve deleted the user.
  2. Perform a query to find the user with that ID. Passing true to withSoftDeleted tells Fluent to include soft-deleted models.
  3. Ensure the user with that ID exists, otherwise throw a 404 Not Found error.
  4. Call restore(on:) on the user to restore that user. Transform the response to 200 OK.

Register the route handler in boot(router:) below, tokenAuthGroup.delete(User.parameter, use: deleteHandler):

tokenAuthGroup.post(
  UUID.parameter, 
  "restore",
  use: restoreHandler)

This maps a POST request to /api/users/<UUID>/restore to restoreHandler(_:). Note how this uses UUID.parameter since User.parameter would return a 404 Not Found error with a soft-deleted model. 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
select id from "User" 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 you can soft delete and restore users, you may want to add the ability properly delete a user. You use force delete for this. Back in Xcode, create a new route to do this, below restoreHandler(_:):

func forceDeleteHandler(_ req: Request)
  throws -> Future<HTTPStatus> {
    // 1
    return try req.parameters
      .next(User.self)
      .flatMap(to: HTTPStatus.self) { user in
        // 2
        user.delete(force: true, on: req)
          .transform(to: .noContent)
    }
}

Here’s what the code does:

  1. Get the user from the parameters and define the callback for when the future resolves. You must do this since Fluent’s convenience delete(on:) for Future<Model> doesn’t support force delete.
  2. Call delete(force:on:) on the model. This bypasses the soft delete and removes the model from the database.

Register the route in boot(router:) below tokenAuthGroup.post(UUID.parameter, "restore", use: restoreHandler) with the following:

tokenAuthGroup.delete(
  User.parameter, 
  "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. If you configure these, Fluent automatically sets and updates the times. To enable this, open Acronym.swift in Xcode. Below var userID: User.ID add two new properties for the dates:

var createdAt: Date?
var updatedAt: Date?

Fluent sets these two dates. For Fluent to know these exist, you must set two keys, similar to configuring the soft delete functionality. Expand the PostgreSQLModel extension for Acronym and add the following:

static let createdAtKey: TimestampKey? = \.createdAt
static let updatedAtKey: TimestampKey? = \.updatedAt

Fluent looks for these keys when creating and updating models. If they exist, Fluent sets the date for the corresponding action. 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)
  throws -> Future<[Acronym]> {
    return Acronym.query(on: req)
      .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 createdAtKey when you create the model. Fluent also sets updatedAtKey when you create the model and any time you update it. Register this route in boot(router:) below acronymsRoutes.get(Acronym.parameter, "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 26, “Database and API Versioning & Migration”. In Terminal, run the following commands:

docker stop postgres
docker rm postgres
docker run --name postgres -e POSTGRES_DB=vapor \
  -e POSTGRES_USER=vapor -e POSTGRES_PASSWORD=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. Close your project in Xcode. Then, in Terminal, enter the following:

touch Sources/App/Models/UserType.swift
vapor xcode -y

This creates a new file for the enum and regenerates the project so Swift Package Manager picks up the new file. When Xcode opens, open UserType.swift and add the following:

// 1
import FluentPostgreSQL

// 2
enum UserType: String, PostgreSQLEnum, PostgreSQLMigration {
  // 3
  case admin
  case standard
  case restricted
}

Here’s what the new code does:

  1. Import FluentPostgreSQL to expose the required types for the enum.
  2. Create a new String enum type, UserType. The type conformances allow Fluent to use UserType in the database and prepare the database correctly. The type must be a String enum to conform to Codable.
  3. Define three types of user access for use in the Vapor application.

Open configure.swift and add the following before migrations.add(model: User.self, database: .psql):

migrations.add(migration: UserType.self, database: .psql)

This adds the migration to MigrationConfig so Fluent prepares the database correctly to use the enum. Note this uses add(migration:database:) rather than add(model:database:) since UserType isn’t a model. Open User.swift and add a new property below var deletedAt: Date? to store the user’s type:

var userType: UserType

Change the initializer to support the new property:

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

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

let user = User(
  name: "Admin",
  username: "admin",
  password: hashedPassword,
  email: "admin@localhost.local",
  userType: .admin)

This sets AdminUser to be an admin type. Open UsersController.swift to make use of this new property. Replace the body of deleteHandler(_:) with the following:

// 1
let requestUser = try req.requireAuthenticated(User.self)
// 2
guard requestUser.userType == .admin else {
  throw Abort(.forbidden)
}
// 3
return try req.parameters
  .next(User.self)
  .delete(on: req)
  .transform(to: .noContent)

Here’s what the new code does:

  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 five parameters with names and values:

  • username: a username of your choice
  • name: a name of your choice
  • email: an email address 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 provides hooks for various aspects of a model’s lifecycle. Fluent exposes the following hooks:

  • willCreate: called before Fluent creates a model.
  • didCreate: called after Fluent creates a model.
  • willRead: called before Fluent reads a model.
  • willUpdate: called before Fluent updates a model.
  • didUpdate: called after Fluent updates a model.
  • willDelete: called before Fluent deletes a model.
  • didDelete: called after Fluent deletes a model.
  • willRestore: called before Fluent restores a soft-deleted model.
  • didRestore: called after Fluent restores a soft-deleted model.
  • willSoftDelete: called before Fluent soft deletes a model.
  • didSoftDelete: called after Fluent soft deletes a model.

These hooks allow you to add additional check to your models, populate or remove fields or add extra steps such as log messages. To demonstrate this, open User.swift and add the following below static let deletedAtKey: TimestampKey? = \.deletedAt in the PostgreSQLUUIDModel extension:

// 1
func willCreate(on conn: PostgreSQLConnection) 
  throws -> Future<User> {
    // 2
    return User.query(on: conn)
  	  .filter(\.username == self.username)
  	  .count()
  	  .map(to: User.self) { count in
        // 3
        guard count == 0 else {
          throw BasicValidationError("Username already exists")
        }
        return self
    }
}

Here’s what the new code does:

  1. Implement willCreate(on:) to perform additional checks before you create a user.
  2. Query the database to get all the users with the new user’s username.
  3. Ensure there are no users with that username, otherwise throw a BasicValidationError. This returns a better error message to the client than the database constraint violation message. Throwing an error cancels the save. Note that you should still use the database to assert that a username is unique in case two users try and register with the same username at the exact same time.

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 five parameters with names and values:

  • username: admin
  • name: Admin
  • email: admin@admin.com
  • password: password
  • userType: admin

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

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 users with all their acronyms. This is commonly referred to as the N+1 problem and, at the time of writing, Fluent provides no easy way to achieve this. You must implement it manually. Open UsersController.swift and add the following at the bottom of the file:

struct UserWithAcronyms: Content {
  let id: UUID?
  let name: String
  let username: String
  let acronyms: [Acronym]
}

This defines a new type to use when returning all the users with their acronyms. Below forceDeleteHandler(_:) add the code to perform the queries:

func getAllUsersWithAcronyms(_ req: Request)
  throws -> Future<[UserWithAcronyms]> {
    // 1
    return User.query(on: req)
      .all()
      .flatMap(to: [UserWithAcronyms].self) { users in
        // 2
        try users.map { user in
          // 3
          try user.acronyms.query(on: req)
          .all()
          .map { acronyms in
            // 4
            UserWithAcronyms(
             id: user.id,
             name: user.name,
             username: user.username,
             acronyms: acronyms)
          }
        // 5
        }.flatten(on: req)
    }
}

Here’s what the new route handler does:

  1. Get all the users from the database.
  2. Use map(_:) to transform each User into Future<UserWithAcronyms>.
  3. Get all the acronyms for the user.
  4. Populate UserWithAcronyms.
  5. Flatten the array of futures to return the array of all users with all their acronyms.

Finally, register the route in boot(router:) below usersRoute.get(User.parameter, "acronyms", use: getAcronymsHandler):

usersRoute.get("acronyms", use: getAllUsersWithAcronyms)

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

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

Joins

The above scenario isn’t very efficient. For a database with a hundred users, you need to make a hundred database queries to get all their acronyms, just for a single request. When getting all acronyms with their users, you can do this more efficiently with a join. Joins allow you to combine columns from one table with columns from another table by specifying the common values. Such as combining the acronyms table with the users table using the user’s ID.

Open AcronymsController.swift and add the following at the bottom of the file:

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

This defines the type to return containing acronym information and a public representation of a user.

Next, add a new route handler to use this below getMostRecentAcronyms(_:):

func getAcronymsWithUser(_ req: Request)
  throws -> Future<[AcronymWithUser]> {
    // 1
    return Acronym.query(on: req)
      // 2
      .join(\User.id, to: \Acronym.userID)
      // 3
      .alsoDecode(User.self).all()
      // 4
      .map(to: [AcronymWithUser].self) { acronymUserPairs in
        // 5
        acronymUserPairs
          .map { acronym, user -> AcronymWithUser in
            // 6
            AcronymWithUser(
              id: acronym.id,
              short: acronym.short,
              long: acronym.long,
              user: user.convertToPublic())
        }
    }
}

Here’s what this new route handler does:

  1. Create a query on the Acronym table.
  2. Join the User table to the Acronym using the shared value - the user’s ID.
  3. Also decode the result from the query into Users.
  4. When the Future resolves, it returns an array of tuples containing the acronyms and users.
  5. Use map(_:) to transform each tuple into AcronymWithUser.
  6. Create AcronymWithUsers from the data returned. Transform the user into a public representation.

Register the route in boot(router:) under acronymsRoutes.get("mostRecent", use: getMostRecentAcronyms):

acronymsRoutes.get("users", use: getAcronymsWithUser)

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

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

Raw SQL

In Fluent, there’s currently no solution for solving the N+1 problem efficiently. You can manually get all users and all acronyms and combine them server-side, but Fluent doesn’t yet provide a way to do this. 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.

Still in AcronymsController.swift, add the following below getAcronymsWithUser(_:):

func getAllAcronymsRaw(_ req: Request)
  throws -> Future<[Acronym]> {
    // 1
    return req.withPooledConnection(to: .psql) { conn in
      // 2
      conn.raw("SELECT * from \"Acronym\"")
      // 3
      .all(decoding: Acronym.self)
    }
}

Here’s what the code does:

  1. Get a database connection from the request to make a query on.
  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, providing type safety.

Register the new route in boot(router:) below acronymsRoutes.get("users", use: getAcronymsWithUser) 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.