Migrating from Vapor 1 to Vapor 2
Vapor 2 is released šš This is a big breaking change, but a good one. We have a small server-side project which is developed with Vapor. When Vapor 2 released, we directly took a look at what changed. We were a bit š and a bitš©. Vapor 2 has fewer lines of code, the performance is better and has a lot of new cool features. But on the other side, there is too much change that we had to adopt. As a beginner to the server-side and Vapor, it took quite some time. We took some notes while migrating to version 2. First, let me introduce the basic components that Iāll talk about. Weāre using MongoDB as a database. We have two custom middlewares which are for verifying requests and setting up response headers. We use custom Routing with RouteCollection
. Thatās all.
Letās start migrating! šŖš½
We started with the Package.swift file. We were using Vapor 1.5.0 and VaporMongo 1.1.0
dependencies: [
.Package(url: "https://github.com/vapor/vapor.git", majorVersion: 1, minor: 5),
.Package(url: "https://github.com/vapor/mongo-provider.git", majorVersion: 1, minor: 1)
]
Now, weāre using Vapor 2.0 and MongoProvider 2.0.0 (was named VaporMongo before). MongoProvider comes with FluentProvider which is the new package. āThe Fluent provider package adds Fluent to project and adds some additional, Vapor-specific conveniences like HTTP conformances.ā
dependencies: [
.Package(url: "https://github.com/vapor/vapor.git", majorVersion: 2),
.Package(url: "https://github.com/vapor/mongo-provider.git", majorVersion: 2)
]
Then the moment came. Ran vapor clean && vapor build
command and BAM! Tons of errors. š© There are a lot of things to do.
Model
The Model
protocol has moved to the FluentProvider
package. We had to import FluentProvider
to our Model
files. In Model
, query()
method change to makeQuery()
. So, we were using User.query()
and we changed it to User.makeQuery()
to run custom queries on User model. The other entity methods like all
, find
, chunk
, count
are still same.
Fluent models now require Storable
protocol conformance for database operations. This protocol requires only one property which handles id
and exists
propertiesā jobs. We donāt need to handle these properties manually anymore. We put let storage = Storage() as a property of every model. Instead of Node
struct and NodeConvertible
protocol, we now have Row
struct and RowConvertible
protocol. It has two new methods one for initializing model and the other for making a row. Even though I donāt get the idea of a Row in databases like MongoDB, the implementation looks better now. So, we had NodeConvertible
implementations before like this:
init(node: Node, in context: Context) throws {
id = try node.extract("_id") // Trick for MongoDB database
name = try node.extract("name")
score = try node.extract("score")
}
func makeNode(context: Context) throws -> Node {
return try Node(node: [
"_id": id, // Trick for MongoDB database
"name": name,
"score": score
])
}
We migrated to RowConvertible
and changed our code to following:
init(row: Row) throws {
name = try row.get("name"")"
score = try row.get("score")
}
func makeRow() throws -> Row {
var row = Row()
try row.set("name", name)
try row.set("score", score)
return row
}
Lastly, we need to send id
s in the response body. We had to conform JSONRepresentable
protocol and create custom JSON
for our models.
extension User: JSONRepresentable {
func makeJSON() throws -> JSON {
var json = JSON()
try json.set("id", id?.string)
try json.set("name", name)
try json.set("score", score)
return json
}
}
Note: If you still need NodeConvertible
, NodeRepresentable
protocolās function syntax has been changed.
func makeNode(context: Context) throws -> Node
migrated to this: func makeNode(in context: Context?) throws -> Node
RouteCollection protocol
The only change in RouterCollection
protocol is the method definition.
class OurCoolCollection: RouteCollection {
typealias Wrapped = HTTP.Responder
func build<Builder: RouteBuilder>(_ builder: Builder) where Builder.Value == Responder { ... }
}
migrated to
class OurCoolCollection: RouteCollection {
func build(_ builder: RouteBuilder) throws { ... }
}
Also drop.collection(OurCoolCollection())
is now throwable.
Request Parameters
Because of technical limitations, we had a limit for the request parameters before. Now, there is no limit because the implementation is totally changed. There is a new Parameterizable
protocol. Anything that conforms to Entity
protocol, can conform Parameterizable
without any extra work. So, every Fluent model can be Parameterizable
easily with only conforming like extension User: Parameterizable { }
. In our custom collection, implementation of endpoints was like this:
let users = builder.grouped("user")
let userController = UserController()
users.post("", User.self, handler: userController.create)
with only one change, it became like this
let users = builder.grouped("user")
let userController = UserController()
users.post("", handler: userController.create)
And the implementation of create
function in the controller was like this:
func create(request: Request, user: User) throws -> ResponseRepresentable
Instead of the function parameters, we can get all request parameters directly from the request. So, the syntax of function became like following
func create(request: Request) throws -> ResponseRepresentable {
let user = try request.parameters.next(User.self)
}
String
and Int
data types automatically conforms Parameterizable
protocol. It means that we can use them as parameters. We also have an endpoint which only requires the id of User as a String in the request. In Vapor 2, new implementation is like this:
func someFunction(request: Request) throws -> ResponseRepresentable {
let id = try request.parameters.next(String.self)
}
and in the get
function, we just called .parameter
property of Parameterizable
protocol.
users.get("someEndpoint", String.parameter,
handler: userController.someFunction)
Droplet & Configuration
All of the Droplet parameters are now constants. Changing them after initialization is not allowed anymore. Vapor 2 has new Config concept which we can set up our Dropletās configurations like middlewares, providers, preparations etc.. Droplet has two initialization methods. We used init(_ config: Config)
. Creating the configuration and setting it up separately looks cleaner.
Our Droplet was written in Vapor 1 and it was something like that:
let drop = Droplet()
drop.middleware.insert(try CORSMiddleware(configuration: drop.config), at: 0)
drop.middleware.append(DummyMiddleware())
drop.middleware.append(SomeMiddleware())
drop.collection(OurCoolCollection())
try drop.addProvider(VaporMongo.Provider.self)
drop.preparations.append(User.self)
Now, we first created the config with
let config = try Config()
next, we added middleware to config
config.addConfigurable(middleware: DummyMiddleware(), name: "dummy")
config.addConfigurable(middleware: SomeMiddleware(), name: "some")
lastly, we added these middlewares to our droplet.json
file. Vapor 2 puts the middlewares in order according to droplet.json
. For example, if you want your AwesomeMiddleware
to run at first, add it to the top of middlewares array at the droplet.json
file. After adding the middlewares to the droplet.json
, it became like this
{
"server": "engine",
"client": "engine",
"console": "terminal",
"log": "console",
"hash": "crypto",
"cipher": "crypto",
"middleware": [
"cors",
"abort",
"type-safe",
"date",
"dummy",
"some"
]
}
As mentioned before, we now have the new FluentProvider
. We had to add fluent.json
for under Config folder. Otherwise, it crashes the app when we run. Vapor allows you to define the idType
for the database as UUID format in JSON file instead of creating manually in every model. Also, we have to define our driver
in here. Weāre using MongoProvider
and because of that our database driver
is mongo
. We added this provider with only one line
try config.addProvider(MongoProvider.Provider.self)
and we defined a fluent.json
file like
{
"idType": "uuid",
"driver": "mongo"
}
Letās look at preparations quickly. There is no extra work like Provider. We just added them to the config.
config.preparations.append(User.self)
Now, we can initialize our Droplet
with the configuration in one line, thatās all.
let drop = try Droplet(config)
Our Config
is not done yet. It seems hash
and cipher
have mandatory keys right now. So, we had to change our crypto.json
file. Both of them now requires three keys, method
, encoding
, and key
. Our crypto.json
file was like a dummy one before
{
"hash": {
"method": "sha256",
"key": "password"
},
"cipher": {
"method": "chacha20",
"key": "passwordpasswordpasswordpassword",
"iv": "password"
}
}
Now, it looks smarter
{
"hash": {
"method": "sha256",
"encoding": "hex",
"key": "FjavUZaqfjjMg4q6722XXY3H4Yvmm39YorHQJ2uslPU"=
},
"cipher": {
"method": "aes256",
"key": "pHhMTJkxkykYn6cJu8fmtVUHkIx/61BTM5tYEwPsYa0=",
"encoding": "base64",
}
}
The last thing for the config was the url
. Itās mandatory in mongo.json
for MongoProvider
configuration. Even if we gave host, port, username, password, and database parameters in JSON file, it insistently asked the url
key. So, we just added url
key to mongo.json
file in the following format. The key point is, MongoKitten
requires this url
in a certain format. It has to start with mongodb://
.
"url": "mongodb://user:password@127.0.0.1:27017/test_db"
Abort
Abort is not enum anymore. It has converted to struct with three parameters.
var status: Status { get }
var reason: String { get }
var metadata: Node? { get }
We were using the Abort
like following
throw Abort.custom(status: .badRequest,
message: "Why is this error happening? Because: \(error)")
throw Abort.notFound
and it is migrated to cleaner implementation
throw Abort(.badRequest,
reason: "Why is this error happening? Because: \(error)")
throw Abort(.notFound)
Update:
servers.json file is simplified and renamed to server.json. Basic server.json can be found in the documentation like this:
{
"//": "The $PORT:8080 call tells the json file to see if there",
"//": "is any value at the 'PORT' environment variable.",
"//": "If there is no value there, it will fallback to '8080'",
"port": "$PORT:8080",
"host": "0.0.0.0",
"//": "It's very rare that a server manages its own TLS.",
"//": "More commonly, vapor is served behind a proxy like nginx.",
"securityLayer": "none"
}
Last words
Adoption to the latest versions with breaking changes is generally hard. But when it comes to performance, clean code and other new features, walking around with the old version become redundant. Some people declare this as living on the edge. Although I agree with this on some level, I generally try to use latest versions when they came out. But I always give it a try with some trial projects, not directly on production code.
Further: