This article will cover how to work with Futures and returning them in route handlers in Vapor 3. Let's get straight to it!
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).
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.
-> Future<View>
-> Future<HTTPStatus>
-> Future<Response>
. This is useful for redirecting on register/logins.Content
protocol), it will be -> Future
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.
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.
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
.
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!)
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.
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.
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:
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.
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)
}
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:
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.
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!