What's New In Vapor 3

Written by Tim on Monday, January 29, 2018. Last edited on Friday, March 23, 2018.

Vapor 3 is currently in Beta and still changing and evolving as the bugs and issues are shaken out. I'll keep this post up to date as the framework evolves until the final 3.0.0 release is tagged.

Vapor 3

Vapor 3 has been one of the most exciting, world-changing, rug-from-out-underneath-you, interesting and at times head-banging-on-the-wall experiences since I started with Vapor! The transition to Vapor 3 is a game changer, and nothing compared to migrating to Vapor 2. Honestly I think if you are coming to Vapor 3 completely fresh with no experience of Vapor 2, you are probably going to have a better time! However, don't despair, there is light at the end of the tunnel and once you get your head around async and Futures, you will be thankful. Going back to Vapor 2 having been focused on Vapor 3 for so long sometimes feels as painful as having to go from Swift back to Java or JavaScript!

Vapor 3 is effectively a complete redesign of the framework and actually closely follows the evolution of Swift itself. With Swift you had Swift 1 when it first came out, Swift 2 which improved a few things and added a few features as people used it more and then Swift 3 broke the world as everyone learnt how they wanted the language to work and how using it day-to-day really was and it set the foundations for the future of Swift. Going to Swift 4 was (almost) painless. Vapor 3 is exactly the same. Vapor 2 added some cool features such as configuration and proper routing but Vapor 3 has built the foundations for Vapor to grow and evolve into the leading server-side Swift framework it has become for years to come. It will be a painful, but necessary transition, much in the same way that Swift 2 to Swift 3 was. Whilst I don't agree with every design decision that was made, I fully support them and understand them.

Vapor 3 will also require Swift 4.1 due to (mainly) the conditional conformance support that it brings, and you can read more about that decision on the Vapor blog. For details on how to get Swift 4.1 (without installing Xcode beta - I believe you need a later toolchain that the one shipped with Xcode 9.3 beta 1), see this gist.

Hopefully that hasn't put you off too much! Let's now take a look at all of the fun stuff that you can now make the most of.

Codable

Vapor 3 now brings full, native, support for Swift 4's Codable and it is used everywhere. From decoding data, accessing the database, reading queries and interacting with Leaf, Codable seems to be in every corner of Vapor, and this makes it awesome! In Vapor 2 your models would need to implement makeRow() and init(row:) to work with the database. Then if you wanted to return them as JSON or initialise them from JSON data you would also have to implement makeJSON() and init(json:). Then if you wanted to interact with Leaf or pass them to most other places in Vapor you also needed to implement makeNode() and init(node:). Model classes would become massive and due to the requirement to implement the Storage object you had to add Extendable hacks to make your models conform to Fluent's Model in an extension. This is no more! Let's say you have a model that looks like:

final class User {
    var id: UUID?
    var name: String
    var username: String
    var password: String
}

To make this model conform to Model in Vapor 2 was painful and you would end up with 50 lines of code. In Vapor 3 (or rather, Fluent 3) you simply write:

extension User: Model {
    typealias Database = MySQLDatabase
    static let idKey = \User.id
}

There are even convenience models that allow you to get this down to a single line!

extension User: MySQLModel {}

If you need to return your user as JSON you can simply return it! Any interactions with the database are also all handled with Codable.

Parsing incoming data is just as easy as well. If you are sent a JSON payload that contains the JSON for your user you can simply do try req.content.decode(User.self) and it takes care of everything! Well, this isn't entirely true, you simply need to add an empty conformance to Content for your model and it will do it all for you.

Content is used for both encoding and decoding data. Complex JSON payloads can be described in structs and you can simply return these for responses or decode them to get the data from requests. This transfers to forms as well - if you are receiving data from a form you can simply set the defaultMediaType on your Content object to MediaType.urlEncodedForm and it will decode all of the incoming data for you.

This means no more Node type - and good riddance too! It was a useful stopgap and achieved its aim, but thankfully with Codable it becomes a lot easier to handle different types of data (and means you get to write far less code as well 🎉)

Async

By far the biggest change in Vapor 3 is the move from the synchronous, blocking world of Vapor 2, to Vapor 3's async, non-blocking architecture. This is going to be the biggest pain point for anyone coming to Vapor 3 and is going to take some time to get used to and understand. Once you have wrapped your head around it is absolutely fine, and writing code for it is easy, but there is a learning curve. If you have experience using one of the modern Javascript frameworks, such as Angular or React, then the async/promise concepts are very similar and you may have already climbed that hill!

Motivations

To explain why Async has been chosen, we need to take a step back and look at the problems that Vapor 2 (and all blocking frameworks, including Kitura and Perfect) have. Imagine you have a single thread in a synchronous world, if you then make a call to a database or external API you will be blocking that thread until you get a response. You can't just return since you must return a response to the client so what happens is your thread is sat there idle while any other incoming requests can't use it. The way around this way to simply spin up more threads, which would be fine if our CPUs had hundreds or thousands of cores, but they don't. Most deployments will have access to only a few physical threads they can use, so frameworks create virtual threads so you can handle as many incoming requests as you want. But context switching between these virtual threads is extremely expensive, especially when you consider that you need to ensure that any data accessed is thread safe as any physical thread can pick up a virtual thread.

In a non-blocking world, you can simply 'put the thread down' until you get a response and wrap it in a promise that when it returns, you will execute the next stage of code. This frees up the thread and allows you to service any requests that are waiting. If you have a mix of requests that require access to fast APIs (think Redis) and just returning static content or strings, you can handle multiple requests on that thread in the same time that the blocking thread could handle the original request. Asynchronous, non-blocking architectures provide a huge performance boost and this is the reason why all of the most performant frameworks use them.

From a coding point of view however, this slightly changes your approach to how you write your code. In Vapor, if you make a query on the database to get all of the users, this function call will no longer return [User]. Instead the return type will be a future array of users, Future<[User]>, because at some point in the future the database query will return. For the most basic uses of Vapor you may be able to avoid touching futures but you will very quickly see them everywhere and have to deal with them.

Whilst it can be a little bit confusing at first it is actually fairly simple - you just need to get used to returning Futures and dealing with them. There are a couple of helpers that you can use to unwrap your code when you first get started:

  • wait() - this will return once the future has completed, but it cannot be used from the EventLoop.
  • blockingAwait() - this does what it says on the tin - it will block the thread until the future has completed. Obviously this should never be used in production, but is really helpful for writing your tests and avoiding nesting and/or XCTestExpectation hell.

Dealing With Futures

The real way to deal with futures is to take the plunge and unwrap them! Almost all your route handlers will return a future of some sort, so you will end using return in your code a lot earlier than expecting, to return futures from the start, then either chaining the futures, or nesting them if you need access to them. There are two methods that you will use, depending upon what your code does. If your callback will return a future, you can use the flatMap call. If your callback returns a normal object you simply use the map function. It is much easier to explain this in code however, so let's see an example!

If I want to query the database for all the users and return the first one I would write my route handler like:

func getFirstUser(_ req: Request) throws -> Future<User> {
    return User.query(on: req).all().map(to: User.self) { users in
        return users[0]
    }
}

There is a lot going on in that code so let's break it down. First the function signature is pretty similar to Vapor 2 except that you will now define the return type explicitly, which isn't really an issue. Next we make the query on the database; this query needs a thread to execute the query on so we pass it the request so the work will be performed on the same thread. We also return this immediately since it is a future, hence the first keyword of our handler being return. Once we have executed the query we will then map the future to the return type - we do this because Swift's inference just isn't advanced enough to work out what is going on without the compiler blowing up or you getting errors that are completely unrelated. Finally, this map function takes a completion handler to execute once the future returns, and in our case we simply return the first user. Since the first user is a User type we use map.

If we were to update the first user and save it, our code would look something like:

func updateFirstUser(_ req: Request) throws -> Future<User> {
    return User.query(on: req).all().flatMap(to: User.self) { users in
        var user = users[0]
        user.name = "Alice"
        return user.save(on: req)
    }
}

This code looks fairly similar to the above method with the exception that we are using flatMap instead. This is because save() returns a future (because again it is a database call) so we use flatMap. (It also returns the future saved user, which is why the return type is the same). Note that again we need to give save() the thread to do the work on.

So if there is one thing you need to remember - return a future, use flatMap. Return a non-future, use map.

The final function to mention that is very useful is the transform() function. Quite often we want to do something with our future and then just return something completely unrelated. To do this we can use the transform function to transform a future into a different object. So the final code I'll use to demonstrate this is to delete the user:

func deleteFirstUser(_ req: Request) throws -> Future<HTTPStatus> {
    return User.query(on: req).all().flatMap(to: HTTPStatus.self) { users in
        return users[0].delete(on: req).transform(to: HTTPStatus.noContent)
    }
}

Here when we delete the user, we just want to return an OK response, in this case 204 No Content. Our delete() call returns Future<Void> (note again we provide the thread to do the work on) and we can then use the transform function to turn our response into the HTTPStatus.noContent.

The final thing to note is that you will have noticed that we are telling our async functions what thread to do their work on. This is because there is no guarantee of thread safety in Vapor 3. This is because it would be prohibitively expensive to guarantee this, both in terms of performance and code complexity, risk of deadlocks etc, so the framework doesn't. This is in line with other top frameworks and realistically not an issue, you just need to be careful about how you use dependencies and services.

Services

Which leads us nicely onto the next section! As you can handle multiple requests at the same time now, you are more likely to hit issues with thread safety. With the lack of thread safety it means that you can't inject in things that you need into your controllers, such as loggers, HTTP Clients, BCryptHashers etc, since they may crash! It also means that writing singletons becomes dangerous! However this doesn't mean that your code has to become poorly architected - you simply move from the dependency injection pattern to a service locator pattern. Instead of passing in your dependencies, you make them from the request. This does make things slightly harder to test, but the performance benefits mean that it is necessary!

How this looks is in code is as so - imagine I want to make a request to an external API using Vapor's client. In Vapor 2 you would probably pass this client into the initialiser of the Controller and use it from there. In Vapor 3 you would probably do something like:

func getInfo(_ req: Request) throws -> Future<Response> {
    let client = try req.make(Client.self)
    return client.respond(to: ...)
}

Then we can register our different Clients for test and for our actual app, which allows us in test to intercept the requests and control the responses. Calling .make() on the request will give you a thread-safe instance of client to use. In Vapor, the Request and Response types are better described as containers. They both have underlying HTTPRequest and HTTPResponse objects respectively, which match up to what you expect with status codes and headers, but the top level objects contain thread information and the services for the app, which is why we can make services from them.

Config

Related to this is the changes to configuration. In Vapor 2 this was all through JSON files in the Config/ directory - this is no more! (Though with Codable it can easily be brought back!) Now Config is a code concept, which means that you get type safety and actual compilation errors if your configuration is incorrect. Related to services, you can register multiple services for the same type and then switch them using through configuration.

Database Support

Database support has improved in Vapor 3 - there are now official, maintained drivers for MySQL, Postgres and MongoDB. Even better, these are all pure Swift and quick! No more having to install libpq-dev just to get your code to compile on Linux.

With these native changes, the models can inherit the full features of the underlying database - need to do geometry calculation? You can now use Fluent to perform PostGIS queries. If you are using MongoDB then you no longer have to describe your schema. However, whilst this gives you more power it does come with a disadvantage in that you no longer get the easy flexibility for using an in-memory DB for test and then a real DB for production. It is still possible, you just need to use a lot of generics throughout your code, which can make things a bit messy and difficult to understand, but this is the trade off that needs to be made. Have a look at the SteamPress Vapor 3 branch models for examples on how to do this.

Another awesome feature of Fluent 3 is automatic migrations. In Fluent 2 you had to implement the prepare and revert functions for all of your models to make sure they were added to the database correctly - now this is all automatically inferred! 🙌 This means even less code! And you can still override the default implementations if you need to do anything out of the ordinary, or change your tables at a later date.

Key Path Support

Finally another awesome feature that comes in Fluent 3 is support for Key Paths and their use with queries. Previously you set your column names with Strings in a throwback to the Objective-C 'stringly-typed' objects. Now with key paths, you get compile time safety when querying your database. If I want to filter I can now do:

try User.query(on: req).filter(\.name == nameSearchTerm)

Where name is a property on your User model and nameSearchTerm is an object of the same type. No more misspelling column names and if you try and compare objects of different types that can't be compared you get a compile time error!

Wrap Up

Hopefully that quick overview of Vapor 3 gives you an insight into the awesomeness of Vapor 3 and the learning curve and challenges you are going to experience! I'm currently porting SteamPress over and there is a lot of code that is being removed and a lot of route handler logic that needs reworking to use Futures. Migrating won't be easy but it will be worth it in the long term as Vapor becomes settled into its vision for the future.

There is a beta branch on the API template which you can get started going with:

vapor new Vapor3Test --branch=beta

Remember - you'll need Swift 4.1 to build it

Be warned however that currently there isn't a ton of information out there as things are changing a lot still, so you may need to get used to reading source code and tests! Hopefully over the coming few weeks as the API settles down the documentation will be rounded out, but as always there is a very active #beta channel in Slack if you get stuck!