Transforming From Vapor 2 to Vapor 3

Vapor
Written by Caleb Kleveter

You may be familiar with one of the products we develop here at Skelpo called ReviewSender; it allows Amazon merchants to manage the reviews on their products, receive notifications for new reviews, and other features. Development for ReviewSender started in July of 2015 using PHP as its backend. While this worked with the customer base that we had, as the amount of our users began to grow, we needed a more maintainable and faster backend than PHP was providing us. This is where Vapor comes in.

In October of 2017, work for moving our backend from PHP to Vapor 2 started, v2 being the latest version available at the time. If you haven’t heard of Vapor before, it’s a server-side framework written in Swift. This was a huge improvement over our earlier codebase and the transition was smooth. Our dev team was very happy with the result: a cleaner and more maintainable backend.

The twist to this transition was that Vapor 3 was in development at the time we are converting, the first beta being released on February 9th. We were pretty excited about what Vapor 3 had to offer. In fact, we were so excited we started porting to v3 on January 30th. Yep, over a week before the beta was released.

There are several reasons we were so excited about porting from v2 to v3:

  • It’s asynchronous: Vapor 2 is blocking/synchronous whereas Vapor 3 is non-blocking/asynchronous. This means that, although it can be harder to write, it is around twice as fast and a single thread can handle multiple clients at a time.
  • Code serialization: Because of updates to Swift in versions 4 and 4.1, Vapor 3 is able to serialize a lot of code we had to previously write manually, allowing us to remove hundreds of lines of code. One of the best examples of this is how models are defined. In Vapor 2, we had to do something like this:
import Vapor
import Fluent

final class User: Model {
        let storage: Storage = Storage()
        let username: String
        let email: String
        let password: String
        init(username: String, email: String, password: String) {
            self.username = username
            self.email = email
            self.password = password
        }
}

extension User: RowConvertible {
        func makeRow()throws -> Row {
            var row = Row()
            try row.set("username", username)
            try row.set("email", email)
            try row.set("password", password)
            return row
        }
        init(row: Row)throws {
            self.password = try row.get("password")
            self.email = try row.get("email")
            self.username = try row.get("username")
        }
}

extension User: JSONConvertible {
        func makeJSON()throws -> JSON {
            var json = JSON()
            // We don't want to return the user's password hash in the JSON.
            try json.set("username", username)
            try json.set("email", email)
            return json
        }
        convenience init(json: JSON)throws {
            try self.init(
                username: json.get("username"),
                email: json.get("email"),
                password: json.get("password")
            )
        }
}

extension User: Preparation {
        static func prepare(database: Database)throws {
            try database.create(self) { builder in
                builder.id()
                builder.string("username")
                builder.string("email")
                builder.string("password")
            }
        }
        static func revert(database: Database)throws {
            try database.delete(self)
        }
}

59 lines for a single, 3 property model (I am not including storage because that is required by Fluent for storing the model in the database)

In Vapor 3, we can trim it down to a mere 27 lines (with the comment):

import Vapor
import FluentMySQL

final class User: Content, MySQLModel, Migration {
    var id: Int?
  
    let username: String
    let email: String
    let password: String
    
    init(username: String, email: String, password: String) {
        self.username = username
        self.email = email
        self.password = password
    }
}

// This struct exists so we can return JSON without the user's password hash.
struct UserResponse: Content {
    let username: String
    let email: String
    
    init(user: User) {
        self.username = user.username
        self.email = user.email
    }
}
  • Greater safety: Another result of using Swift 4 was greater type safety through the Codable protocol and the KeyPath types, meaning we were less likely to have run time errors or bugs. An example of KeyPath is being able to use .filter(\.id == 3) verses .filter("id", 3), guaranteeing that you are checking with the correct type and you didn’t mistype the property name.
  • No more JSON configuration files!

An additional benefit of porting to Vapor 3 during the beta period was that we were able to find bugs and missing features in the framework, allowing the official release of v3 to be more stable and complete.

The actual transformation from v2 to v3 is simpler on paper then in practice. There are no automatic migration tools like Swift has, so the method we decided to take was to update the version of Vapor we were using, commenting out the whole project, and then going through, updating one piece at a time. A similar option would be to create an empty project and move blocks of code over to the new project, but it would be more difficult getting Git to work without wiping to whole project history.

The process was tedious, fun, and rewarding. Most of our data structures where reduced to 13 and route handlers to 14 the size of their previous length. Removing that much code does a few things for you:

  • Your app will be faster. Yes, more characters in your app will make your app slower (though the difference won’t be any kind of dealbreaker).
  • Because there is less code for you to keep track of while working on the app, it is easier to find potential pitfalls in your app’s logic or cleaner and more efficient implementations for routes, CRON jobs, etc.

A couple of times we came across a missing feature, but the Vapor team was really good about getting it added quickly. Although we are still working on getting the services converted to v3, we are pleased with the progress we have made and are looking forward to see what is coming next in the future of Vapor!

Comments

Info@skelpo.com

Shoot us an email, we’ll find a way to be helpful.

No pressure. Really.