Working with Futures in Vapor 3

1676 Views

This article will cover how to work with Futures and returning them in route handlers in Vapor 3. Let's get straight to it!

Why Futures?

Without going into too much low-level detail, the biggest difference between Vapor 2 and Vapor 3 is that Vapor 3 is an asynchronous framework. This will make our apps much faster, because they can process more incoming requests at once. It provides a significant performance boost.

Essentially, any operation that takes time--saving objects, retrieving objects, making external requests, for example--returns immediately. That's why you'll find yourself writing return in your route handlers a lot sooner than before. It returns immediately so the application can continue to process more requests while the slower operations are being completed.

However, when it returns immediately, it doesn't return exactly the objects you were looking for--it returns a "future", which is basically like a box or container which may or may not contain the values you were looking for. If you want to actually access the objects you were looking for, you'll need to use map or flatMap (which will be discussed below).

Future Syntax

Let's start where you'll first encounter a future: as a return value in a function signature. For example, you may have the following in your routes.swift file:

    router.get("test") { request -> Future<User> in
            
    }

In Vapor 3, if you want to return a User object, you will no longer return User, but rather a "future User". So, we will no longer declare our return values like -> User, but rather -> Future--or for an array of users, -> Future<[User]>. Simple enough.

Other types of Futures you can return (this is not an exhaustive list):
  • to return a Leaf template, it will be -> Future<View>
  • to return an HTTPStatus, it will be -> Future<HTTPStatus>
  • to return an HTTPResponse, it will be -> Future<Response>. This is useful for redirecting on register/logins.
  • to return any since instance of your model/class (which must conform to the Content protocol), it will be -> Future

Querying the database

Let's look at an example where we query the database for a list of all users, like below:

    router.get("getUsers") { request -> Future<[User]> in
        let users = User.query(on: request).all()
        return users
    }

This is the simplest use case. We simply query the database, pass it our request, and then call .all() to get the users. The users will then be returned as JSON.

Optionals

Suppose we have a user who we would like to find by their id. Fluent offers us a convenience method for this, like so:

    router.get("singleUser") { request -> Future<User> in
        let userId = 4
        return try User.find(userId, on: request)
    }

But watch out! Then you'll get this error:



We are trying to return a Future<User?>, but our route handler specifies that we should return a Future<User>, and these are 2 different types.

So, that's what the unwrap function is for. Change your implementation to the following, unwrapping the optional:

    router.get("singleUser") { request -> Future<User> in
        let userId = 4
        return try User.find(userId, on: request).unwrap(or: Abort.init(HTTPResponseStatus.notFound) )
    }

And, no compile errors!

For a more complete and in-depth look at querying with futures in Vapor 3, see my previous tutorial Introduction to Querying with Fluent in Vapor 3.

Accessing non-Future Values with .map and .flatMap

The important thing to remember about a future, is that it is a different type--if your User model has properties email, password, and username, you will not be able to access those values from a Future<User> object.

If you want to read, update, delete, or manipulate this data in any way, you will have to get an actual instance of your model. You will also need an actual model object if you wish to save data to the database--you can only save an instance of YourModel, not an instance of Future.

And, that's where two very important methods come in to play: .map and .flatMap.

What's the difference?

The difference between .flatMap and .map is the value their completion handler (closure) returns--that's all. Inside the completion handler of both of these methods, you will have access to your objects, and can manipulate them as needed. The completion handler of .map returns a non-future value (such as an instance of User). The completion value of .flatMap returns a Future value. Enough talk, let's go through some examples!

Let's look at an example for saving a user, using the following code:

    router.post("saveNewUser") { request -> Future<User> in // 1
        return try request.content.decode(User.self).flatMap(to: User.self) { user in // 2
                let savedUser = user.save(on: request) // 3
                return savedUser 
        }
    }

Here, at 1, we declare our return value: this function is going to return a Future<User> object. When we call this route, it will return a JSON with a user object.

At 2, several things are happening: First, we decode the content from the request. The result of calling try request.content.decode(User.self) is a Future. Then, we called .flatMap, and pass User.self as the type we are returning. We passed User.self, because that's the type our route handler specified. Lastly, the route closure gives us an instance of the plain User object (user) that we can use to save, or change in some other way.

At 3, we save the user and then return the future. Notice, that the type of savedFutureUser is a Future<User>. We can see this if we inspect the type in Xcode:



Inspecting types in Xcode is actually a great way to debug if you're having issues with mismatching return types (and you'll surely get quite a few of them while getting used to writing futures!)

When to use flatMap vs map?

There is no simple answer to this globally, as it depends entirely on your needs. But, the general rule is: If you want your closure to return a future, use flatMap. If you want it to return a non-future, use map.

How to exclude a model's property from being returned

In the above example, if we save a user successfully, we will get the following response:

    {"email": "anapaix@email.com", "password": "passwordHere", "id": 1}

Wait, password?! We shouldn't be sending back the password back to the client, even if it is hashed. So, we need to exclude that field. Needing to manipulate such data is an example of when we need to use map.

How can we do this using futures? First, create the following struct:

    struct PublicUser: Content {
        var id: Int
        var email: String
    }

This struct represents the data that we want to pass back to the client, instead of a User object.

Then, implement the following method:

    func login(_ request: Request) throws  -> Future<PublicUser> { // 1
        return try request.content.decode(User.self).flatMap(to: PublicUser.self) { user in // 2
            return user.save(on: request).map(to: PublicUser.self) { savedUser in // 3
                let publicUser = try PublicUser(email: savedUser.email, id: savedUser.requireID()) // 4
                return publicUser
            }
        }
    }

Lets go through that code line by line:

At 1, we declare the final type we want to return--a Future<PublicUser>--this PublicUser will only contain an email and id property--leaving out the password.

In 2, we decode the request, and then call flatMap, because we need to return a future.

At 3, we have access to our user object, and call .save on it. However, here we use map. We use map because we know that ultimately, this closure needs to return an instance of PublicUser--and we will need to manually create an instance of PublicUser (in 4). We know that our publicUser is going to be manually constructed, and therefore *not* a future--so we use map.

At 4, we construct the PublicUser object from our saved user, and then pass that back to the user.

Troubleshooting

When working with futures, it is important to distinguish and keep track of return types. Pay attention to the errors Xcode gives you, as there can be easily be confusion when you have to write return 3 or 4 times in your code for just 1 function.

Knowing what you want to return is the best guide for knowing whether you should use map or flatMap. A few things that I personally do that help me:

  • I use the Xcode type inspector. This tells me exactly what type I'm working with, and then I can know whether or not this type is valid to return.
  • If the type inspector is not working in Xcode (because...well Xcode has room for improvement), or if it gives you the "no quick help", try simply typing out the variable in code. The Xcode autocomplete will tell you what type of object it is.
  • If you are unsure what return type a closure is expecting, create an empty implementation of it, and read the compile error Xcode gives you.

Transform

What if you want to run a function, but then simply want to return something unrelated? Maybe I want to create an object, but then simply tell the client that the object was created--and not actually pass back the entire newly created object? For such things, we can use the .transform. method:

    func createPost(_ request: Request) throws -> Future<HTTPStatus> {
        return try request.content.decode(Post.self).flatMap { post in
            return post.save(on: request).transform(to: HTTPStatus.created)
        }
    }

The transform method transforms a Future into a different object, as its name implies.

Styling

Remember our createPost method from above? Some people may find such nesting a little hard to read. If you prefer, you could also write that method in a different style, and chain your futures instead of nesting them, like so:

func createPost(_ request: Request) throws -> Future<HTTPStatus> {
        return try request.content.decode(Post.self).flatMap { post in
            return post.save(on: request)
        }.transform(to: HTTPStatus.created)
    }

A More Complicated Example

Next, I'll share with you a slightly more complicated example that I ran into while rewriting this site in Vapor 3 (it's still a work in progress for now). In my situation, I needed the following flow for logging in a user:

  1. User sends a login request with email and password
  2. I get their password and password from the request
  3. Authenticate the user and make sure their credentials are correct
  4. If correct, generate a new access token for the user
  5. save the access token to the database
  6. send back the token, userId, email, and displayName of the user

The following was my implementation:

    func loginUser(_ request: Request) throws -> Future<AuthenticatedUser> {
        return try request.content.decode(User.LoginRequest.self).flatMap(to: AuthenticatedUser.self) { user in // 1
            let passwordVerifier = try request.make(BCryptDigest.self) 

            return User.authenticate(username: user.email, password: user.password, using: passwordVerifier, on: request).unwrap(or: Abort.init(HTTPResponseStatus.unauthorized)).flatMap(to: AuthenticatedUser.self) { authedUser in // 2

                let newAccessKey = try AccessToken.generateAccessToken(for: authedUser) // 3
                return newAccessKey.save(on: request).map(to: AuthenticatedUser.self) { newKey in // 4
                    return try AuthenticatedUser(email: authedUser.email, id: authedUser.requireID(), displayName: authedUser.displayName, token: newKey.token) // 5
                }

            }
        }
    }

Now, let's go through this step by step:

1: I get a Future<User> by decoding the request with request.content.decode(User.LoginRequest.self). I then flatMap the future to a separate type, AuthenticatedUser, because that is the type I want to return. This type has properties: email, id, displayName, and token. AuthenticatedUser is defined as a struct in another file in my project.

2: Using my password verifier, I authenticate the user, and then call .unwrap to unwrap the optional. Then, I flatMap this result to AuthenticatedUser as well. The authedUser variable in the closure is now an instance of User.

3: I generate an accessKey for this user

4: I save the accessKey, and call map on the result. Inside the map closure, I will have access to my new access key (newKey). Here, I called map because I knew that I wanted to return an instance of AuthenticatedUser, which is not a future. The custom response of AuthenticatedUser (which does not directly map to a column in my database) must be instantiated directly, so I know that if I need to return it, I need to use map.

5: Here I return the authenticated User.

Recommended Reading

Here are a few other sources that are helpful in understanding futures and the asynchronous nature of Vapor 3:

That is all for this article, hopefully it is able to help you work with futures a bit better! If you enjoyed it, consider being the first person to follow VaporForums on Twitter, as I just created the account yesterday!

Happy coding!



Enjoy this article? Consider supporting VaporForums by following us on Twitter! Get the latest Vapor articles, tutorials, and news.