async/await in Server-Side Swift and Vapor

Learn how Swift’s new async/await functionality can be used to make your existing EventLoopFuture-based Vapor 4 code more concise and readable. By Mahdi Bahrami.

5 (4) · 2 Reviews

Download materials
Save for later
Share
You are currently viewing page 2 of 3 of this article. Click here to view the first page.

Leveraging async/await in Vapor

Let’s go back to our Trader project. Open CrateController.swift in the Controllers folder. You’ll see boot(routes:) in CrateController is registering some routes:

func boot(routes: RoutesBuilder) throws {
    let crateGroup = routes.grouped("crates")
    // 1
    crateGroup.get(use: all)
    // 2
    crateGroup.post(use: create)
    // 3
    crateGroup.post("trade", use: trade)
}

In the code above, three routes are registered:

  1. GET HTTP, at /crates endpoint to retrieve all current crates.
  2. POST HTTP, at /crates endpoint to add new crates.
  3. POST HTTP, at /crates/trade endpoint to enable trading of crates.

Look at all(req:), and try to convert it to an async/await function:

private func all(req: Request) throws -> EventLoopFuture<[Crate]> {
    Crate.query(on: req.db).all()
}

Based on what you previously learned, you can append .get() to the crates query, and it will work with async/await. Don’t forget to change the function’s signature as well. The function becomes an async throwing function that returns [Crate], instead of just a normal throwing function that returns an EventLoopFuture<[Crate]>:

private func all(req: Request) async throws -> [Crate] {
    try await Crate.query(on: req.db).all().get()
}

Build and run Trader again, and you won’t see any compile errors. Your first conversion of an EventLoopFuture function to an async/await function is successful!

But, that’s not all. Remember earlier you saw that the two helpers won’t be needed in most cases. So remove the call to get():

try await Crate.query(on: req.db).all()

Build and run again. You’ll see that Xcode successfully runs your app and doesn’t complain about all() not being an async function. How is that happening?

To make using async/await cleaner and nicer, the Vapor team has added a secondary async function for the most popular functions that return an EventLoopFuture. That means, in this case, you have access to two all() functions. The classic one returns an EventLoopFuture, and the modern one is an async function. In the code above, you’re using the async/await variant of all(), and Swift identifies that with no problems.

The same is true about crateGroup.get(use: all). You’ve changed all(req:) from returning an EventLoopFuture to returning an async value. But the Vapor team has already added a secondary async function overload for all route builders. So, here Swift automatically uses the async/await variant of get(use:) and doesn’t complain at all.

This is the whole idea behind the current async/await support in Vapor. The majority of the functions that were using EventLoopFuture now have an async/await version, so you can simply use them and enjoy!

Now, it’s time to convert create(req:) to async/await. create(req:) creates new crates based on the payload you send it and adds them to the database. It currently looks like this:

private func create(req: Request) throws -> EventLoopFuture<[Crate]> {
    let crates = try req.content.decode([Crate].self)
    return crates.create(on: req.db).transform(to: crates)
}

As you learned, there are two simple steps:

  • Change the function’s signature from -> EventLoopFuture<Value> to async throws -> Value.
  • Change the code inside the function to use the async/await variants.

So, after the conversion, this is your function:

private func create(req: Request) async throws -> [Crate] {
    // 1
    let crates = try req.content.decode([Crate].self)
    // 2
    try await crates.create(on: req.db)
    // 3
    return crates
}

Here’s what you’re doing:

  1. You decode the payload that’s sent to you.
  2. Now, you save the decoded crates in the PostgreSQL database.
  3. Finally, you return the crates as the response.

So far, you’ve learned all the basics of using async/await in Vapor. You’ll get back to CrateController later to learn some more advanced stuff about async/await.

Becoming More Fluent Than Ever!

Fluent is one of Vapor’s core packages, and they’ve updated it with async/await support. This means you can await any queries that you’d ever want to make.

You’ve already tried a few different Fluent queries in all(req:) and create(req:) functions. Now, it’s time to see how to use async/await for a Fluent migration.

Updating Fluent Migrations

Using async/await in Fluent migrations is just as easy, with only one difference from the previous sections. Open CreateCrate.swift in the Migrations folder. You’ll see the following migration:

struct CreateCrate: Migration {
    func prepare(on database: Database) -> EventLoopFuture<Void> {
        database
            .schema(Crate.schema)
            .id()
            .field(Crate.FieldKeys.owner, .string, .required)
            .field(Crate.FieldKeys.item, .string, .required)
            .create()
    }
    
    func revert(on database: Database) -> EventLoopFuture<Void> {
        database
            .schema(Crate.schema)
            .delete()
    }
}

By now, you should be able to update the two functions easily on your own. At the end, you’ll have:

struct CreateCrate: Migration {
    func prepare(on database: Database) async throws {
        try await database
            .schema(Crate.schema)
            .id()
            .field(Crate.FieldKeys.owner, .string, .required)
            .field(Crate.FieldKeys.item, .string, .required)
            .create()
    }
    
    func revert(on database: Database) async throws {
        try await database
            .schema(Crate.schema)
            .delete()
    }
}

Try to build and run Trader. Xcode throws an error that CreateCrate doesn’t conform to Migration:

Xcode showing build error indicating CreateCrate doesn't conform to Migration.

Why is this happening? The reason is very simple. Migration expects prepare(on:) -> EventLoopFuture<Void> and revert(on:) -> EventLoopFuture<Void>, but you’ve just changed those functions to simply be async throws.

The Vapor team has come up with a simple solution. They’ve added the AsyncMigration protocol. It’s the same as the Migration protocol, but accepts async prepare(on:) and revert(on:) functions.

Now, replace Migration with AsyncMigration:

struct CreateCrate: AsyncMigration {
...

AsyncMigration itself conforms to Migration under the hood, so CreateCrate still conforms to Migration. This means you can use CreateCrate as if nothing has changed, and you won’t need to change any more code anywhere else!

Build and run Trader. You’ll see that everything compiles and Trader runs successfully.

Generally speaking, if you were previously using a protocol/type that uses EventLoopFuture, you can expect an async/await variant of it. Usually, you can identify the async/await variants using the Async prefix that they have. For example, in this case, AsyncMigration is the async/await variant of Migration, so it has an Async prefix.

Understanding Concurrent Loading

Now, it’s time to migrate your remaining routes to async/await. Your current knowledge will lead you to a suboptimal conversion.

Open CrateController.swift and look at tradeOne(on:tradingSides:):

private func tradeOne(
    on db: Database,
    tradingSides: TradeItem
) -> EventLoopFuture<HTTPStatus> {
    // 1
    Crate.query(on: db)
        .filter(\.$id ~~ [tradingSides.firstId, tradingSides.secondId])
        .all()
        .tryFlatMap { bothCrates in
            // 2
            guard bothCrates.count == 2 else {
                throw Abort(.badRequest)
            }
            let crate1 = bothCrates[0]
            let crate2 = bothCrates[1]
            // 3
            (crate1.owner, crate2.owner) = (crate2.owner, crate1.owner)
            // 4
            let saveCrate1 = crate1.save(on: db)
            let saveCrate2 = crate2.save(on: db)
            // 5
            return saveCrate1.and(saveCrate2).transform(to: .ok)
        }
}

In the code above:

  1. You retrieve the current crates for each side of the trade. Remember, TradeItem is only a container for ids of two crates, whose items will be traded by the app. The query finds all crates that have either of the crate ids.
  2. You make sure you have two crates retrieved. Then, assign each crate to a variable.
  3. Afterward, you swap the items of the crates. crate1 will have crate2‘s item, and vice versa. This is where the trading happens.
  4. Then, you save the new crates in the database.
  5. Finally, you return the result of the saving process and transform it to a simple 200 OK HTTP status.

Now, you’ll transform tradeOne(on:tradingSides:) to an async/await function. By the end of the process, you should have code similar to this:

private func tradeOne(
    on db: Database,
    tradingSides: TradeItem
) async throws -> HTTPStatus {
    // 1
    let bothCrates = try await Crate.query(on: db)
        .filter(\.$id ~~ [tradingSides.firstId, tradingSides.secondId])
        .all()
    // 2
    guard bothCrates.count == 2 else {
        throw Abort(.badRequest)
    }
    let crate1 = bothCrates[0]
    let crate2 = bothCrates[1]
    // 3
    (crate1.owner, crate2.owner) = (crate2.owner, crate1.owner)
    // 4
    try await crate1.save(on: db)
    try await crate2.save(on: db)
    // 5
    return .ok
}

Here, you:

  1. Retrieve the current crates for each side of the trade.
  2. Make sure you’ve retrieved two crates. Then, assign each crate to a variable.
  3. Swap the items of the crates. crate1 will have crate2‘s item, and vice versa. This is where the trading happens.
  4. Save the new crates in the database.
  5. Return a 200 OK HTTP status code.

Build and run, and you might see some errors indicating that trade(req:) is unhappy with tradeOne(on:tradingSides:). That’s normal, as you just migrated tradeOne(on:tradingSides:) to async/await without changing how the function is called in trade(req:).

So, you need to convert trade(req:) to async/await. Right now, you have:

private func trade(req: Request) throws -> EventLoopFuture<HTTPStatus> {
    // 1
    let allTrades = try req.content.decode([TradeItem].self)
    // 2
    return allTrades.map { tradingSides in
        tradeOne(on: req.db, tradingSides: tradingSides)
    }
    // 3
    .flatten(on: req.eventLoop)
    // 4
    .transform(to: .ok)
}

Here’s what this code does:

  1. Decodes a TradeItem array from the body of the request sent to you.
  2. Iterates through allTrades and performs trades for each TradeItem using tradeOne(on:tradingSides:).
  3. Now that you have an array of EventLoopFuture<Void>, you flatten it to make it one EventLoopFuture<[Void]>, which contains the results of all trades.
  4. Finally, you transform the result of the tasks to a 200 OK HTTP status.

Try to migrate this function to async/await. By the end of the conversion, you’ll have something like this:

private func trade(req: Request) async throws -> HTTPStatus {
    // 1
    let allTrades = try req.content.decode([TradeItem].self)
    // 2
    for tradingSides in allTrades {
        _ = try await tradeOne(on: req.db, tradingSides: tradingSides)
    }
    // 3
    return .ok
}

In the code above, you:

  1. Decode an array of TradeItem from the body of the request sent to you.
  2. Iterate through allTrades and perform the trades for each TradeItem using tradeOne(on:tradingSides:). You don’t need the final results of tradeOne(on:tradingSides:) calls — all that’s important is that the process doesn’t throw any errors.
  3. Then, return a 200 OK HTTP status.

Do the async/await conversions of trade(req:) and tradeOne(on:tradingSides:) look good to you? You might not have noticed, but this async/await code has some differences from the original EventLoopFuture code in terms of execution.