We've been working on the fourth major release of Vapor for almost a year now. The first alpha version was tagged last May, with the first beta following in October. During that time, the community has done amazing work helping to test, improve, and refine this release. Over 500 issues and pull requests have been closed so far!
Looking back at Vapor 3's pre-release timeline, 7 months passed between alpha.1
and the final release. If history repeats itself, we would reach 4.0.0
sometime in February 2020.
As we near the end of the active development phase, efforts are shifting toward a focus on documentation and polish. Since APIs have mostly settled down at this point, I'd like to take this opportunity to introduce you to some of the exciting changes coming in Vapor 4. Let's dive in.
Vapor 3 introduced Services
, a pure Swift configuration framework which replaced Vapor 2's JSON configuration files. In Vapor 4, we're taking this a step further by leveraging the compiler to make configuring Vapor apps as easy as possible.
Vapor 4's new dependency injection API is now based on Swift extensions rather than type names. This makes services offered by third party packages - and Vapor itself! - more discoverable and feel more Swift-native.
How this works is best explained by example, so let's take a look at some common use cases of the Services API in Vapor 3 and what they would look like in Vapor 4.
In Vapor 3, changing the default HTTP port required overriding the default NIOServerConfig
by registering your own:
// vapor 3
services.register(NIOServerConfig.self) { _ in
return NIOServerConfig.default(port: 1337)
}
In Vapor 4, the server configuration is exposed as a mutable property on Application
:
// vapor 4
app.server.configuration.port = 1337
In Vapor 3, the Leaf
provider required registering a LeafConfig
struct to Services
. In order to tell Vapor to use Leaf by default, a preference was added to the Config
struct:
// vapor 3
services.register(LeafConfig.self) { _ in
return LeafConfig(...)
}
config.prefer(LeafRenderer.self, for: ViewRenderer.self)
In Vapor 4, Leaf
's configuration is another settable property on Application
. A new app.views
property makes it easy to tell Vapor which View Renderer to use with the use
method:
// vapor 4
app.leaf.configuration = LeafConfiguration(...)
app.views.use(.leaf)
Vapor 4 upgrades to SwiftNIO 2.0. This release includes tons of great quality of life improvements, performance enhancements, and awesome features like vendored BoringSSL and pure Swift HTTP/2 implementation.
A huge focus for this release was integration with the new Swift Server Working Group (SSWG) ecosystem. Vapor joined forces with Apple to help define common standards for core fuctionality like Logging and Metrics. Vapor 4 has adopted these new standards with open arms. What this means for you is great logging, metrics, and (soon) tracing that works seamlessly across all of your packages.
Vapor 4's Postgres driver was the first non-Apple package to go through the SSWG's proposal process and become an accepted project. The SSWG incubation process is designed to improve the overall quality and compatibility of the server-side Swift ecosystem. Vapor 4's MySQL driver is in the early stages of proposal, with many more packages to come in the future.
Thanks to efforts by the SSWG and wonderful contributions from the community, Vapor 4 will be the first release to depend on packages from authors other than Vapor and Apple. Namely swift-server/async-http-client, mordil/swift-redi-stack, and kylebrowning/APNSwift. We look forward to continuing this trend going forward.
AsyncHTTPClient is a new pure Swift HTTP client built on top of Swift NIO. This package is intended as a more perfomant and lightweight alternative to URLSession
, especially on Linux. Vapor 4 has adopted this package, replacing URLSession
as the framework's default HTTP client
Vapor 4's toolbox includes an improved vapor new
command that helps customize newly generated projects. Rather than choosing from a limited set of pree-existing templates, the new
command will now ask you which packages you want to include in your new project and produce sample code tailored to your choices. For example, if you select both Fluent and JWT, sample code can be included showing how to integrate the packages together.
$ vapor new hello-world
Would you like to use Fluent? [y/n]:
Fluent 4's model API has been redesigned to take advantage of property wrappers in Swift 5.1. Property wrappers give Fluent much more control over how models work internally, which has been key to enabling long-requested features like a concise API for eager loading.
When declaring models, fields are now declared using the @Field
property wrapper. Identifiers use the special @ID
wrapper:
final class Galaxy: Model {
@ID(key: "id")
var id: UUID?
@Field(key: "name")
var name: String
}
Relations are declared with the property wrappers @Parent
, @Children
, and @Siblings
:
final class Planet: Model {
@ID(key: "id")
var id: UUID?
@Field(key: "name")
var name: String
@Parent(key: "galaxy_id")
var galaxy: Galaxy
}
final class Galaxy: Model {
...
@Children(for: \.$galaxy)
var planets: [Planet]
}
Fluent can now preload a model's relations right from the query builder. Models will automatically include eager-loaded relations when serializing to Codable
encoders.
Using the example from above, a Fluent
query can eager-load its Galaxy
parent with the .with()
query builder method:
let planets = try Planet.query(on: db).with(\.$galaxy).all().wait()
for planet in planets {
print(planet.galaxy) // Galaxy
}
The JSON output for this array of planets might look something like this:
[
{
"id": ...,
"name": "Earth",
"galaxy": {
"id": ...,
"name": "Milky Way"
}
},
...
]
Fluent's new model API also makes it possible to do partial reads and updates on the database. When models fetched from the DB are updated and saved, Fluent now sends only the updated field values to the database.
Vapor 4 includes a new testing framework that makes it easier to test your application using XCTest
. Importing XCTVapor
adds test
methods to your application that you can use to easily send requests:
import XCTVapor
app.test(.GET, to: "hello") { res in
XCTAssertEqual(res.status, .ok)
XCTAssertEqual(res.body.string, "Hello, world!")
}
Applications are tested in-memory by default. To boot an HTTP server and run the tests through an HTTP client, use testable
:
app.testable(method: .running).test(.GET, to: ...) {
// verify response
}
Support for HTTP/2 and TLS is now shipped by default with Vapor 4. HTTP/2 support can be enabled by adding .two
to the HTTP server's supported version set:
app.server.configuration.supportVersions = [.two]
TLS can be enabled by setting the server's TLS configuration struct:
app.server.configuration.tlsConfiguration = .forServer(...)
It's important to not that hosting your app behind a reverse-proxy like NGINX is still strongly recommended in production.
Vapor's Content
APIs now operate synchronously:
let newUser = try req.content.decode(CreateUser.self)
print(newUser) // CreateUser
This improvement is thanks to a new default policy on route handlers to collect streaming HTTP bodies before calling the handler. HTTP body collection can be disabled when registering routes:
app.on(.POST, "streaming", body: .stream) { req in
// req.body.data may be nil
// use req.body.collect
}
In addition to new request body collection strategies, request body streaming now supports backpressure. req.body.drain()
, which streams incoming body data, now returns a EventLoopFuture
. Until this future is completed, further request body chunks will not be transferred from the operating system. This allows Vapor apps to stream extremely large files directly to disk without ballooning memory.
Vapor's multipart parsing package MultipartKit
has been rewritten to support streaming multipart/form-data
uploads. This allows you can benefit from backpressure with both direct and form-based file uploads.
Close attention to graceful shutdown has been given to all Vapor types that deal with long-lived resources. Application
and many other types now have close()
or shutdown()
methods which must be called before they deinitialize:
let app = Application()
defer { app.shutdown() }
Requiring explicit shutdown methods is a pattern adopted from SwiftNIO and often helps reduce bugs. These shutdown methods also help prevent reference cycles from leaking memory in your application.
Alongside stricter adherence to good graceful shutdown practices, Vapor's HTTP server now supports NIO's ServerQuiescingHelper
by default. This handler helps to ensure that any in-flight HTTP requests are given time to complete after a server initiates shutdown.
Vapor's Command
APIs have also seen improvements thanks to property wrappers. Command
s now define a Signature
struct which uses wrapped properties to declare accepted arguments. When the command is run, the signature is decoded automatically and passed to the run function.
Available property wrappers are @Argument
, @Option
, and @Flag
:
final class ServeCommand: Command {
struct Signature: CommandSignature {
@Option(name: "hostname", short: "H", help: "Set the hostname")
var hostname: String?
@Option(name: "port", short: "p", help: "Set the port")
var port: Int?
@Option(name: "bind", short: "b", help: "Set hostname and port together")
var bind: String?
}
func run(using context: CommandContext, signature: Signature) throws {
print(signature.hostname) // String?
}
}
A new APNS integration package will ship its first release alongside Vapor 4. This package is built on the great work done by Kyle Browning with APNSwift.
This package integrates APNSwift
into Vapor's application and request types, making it easy to configure and use:
import APNS
import Vapor
try app.apns.configuration = .init(
keyIdentifier: "...",
teamIdentifier: "...",
signer: .init(file: ...),
topic: "codes.vapor.example",
environment: .sandbox
)
app.get("send-push") { req -> EventLoopFuture<HTTPStatus> in
req.apns.send(
.init(title: "Hello", subtitle: "This is a test from vapor/apns"),
to: "..."
).map { .ok }
}
This new package is located at vapor/apns.
As first described on the Swift forums, Leaf's new body syntax is complete and will ship with Vapor 4.
This change replaces Leaf's usage of curly braces with an #end
prefix syntax:
#for(user in users)
Hello #(user.name)!
#endfor
Leaf 4 also has new syntax for template inheritance:
base.leaf:
<html>
<head><title>#import("title")</title><head>
<body>#import("body")</body>
</html>
hello.leaf:
#extend("base"):
#export("title", "Welcome")
#export("body"):
Hello, #(name)!
#endexport
#endextend
And the result when compiled with the context ["name": "Vapor"]
:
<html>
<head><title>Welcome</title><head>
<body>Hello, Vapor!</body>
</html>
Jobs
, a task queuing system for Vapor, will have its 1.0 release alongside Vapor 4. This package allows you to define job handlers for running long-running tasks in a separate process. Your Vapor route handlers can quickly dispatch jobs to these handlers to keep your application fast without compromising error handling.
Job handlers are declared using the Job
protocol and must implement a dequeue
method:
struct Email: Codable {
var to: String
var message: String
}
struct EmailJob: Job {
func dequeue(_ context: JobContext, _ email: Email) -> EventLoopFuture<Void> {
print("sending email to \(email.to)")
...
}
}
Job handlers are then configured using Jobs
' convenience APIs:
import Jobs
import Vapor
app.jobs.add(EmailJob())
Start the job handling process(es) using the new jobs
command:
swift run Run jobs
Once set up, jobs can easily be dispatched from route handlers, using the Request
:
app.get("send-email") { req in
req.jobs.dispatch(EmailJob.self, Email(...))
.map { HTTPStatus.ok }
}
Once dispatched, the job will be later dequeued and run in the separate jobs process. If any errors occur, the EmailJob
handler will be notified.
Jobs also supports scheduling jobs to run at certain times using a new, fluent schedule building API:
// weekly
app.jobs.schedule(Cleanup())
.weekly()
.on(.monday)
.at("3:13am")
// daily
app.jobs.schedule(Cleanup())
.daily()
.at("5:23pm")
// hourly
app.jobs.schedule(Cleanup())
.hourly()
.at(30)
This new package is located at vapor/jobs.
Vapor 3's crypto package has been refactored to mirror Apple's CryptoKit
APIs. This package is now called OpenCrypto and still depends on linking the system's OpenSSL. This change makes it easier for Apple platform developers to use Vapor's crypto APIs and significantly reduces maintenance complexity.
Furthermore, packages that require crypto functionality will now be able to target both server-side use cases and platforms that don't support linking OpenSSL (like iOS) by dynamically importing either CryptoKit
or OpenCrypto
:
#if canImport(CryptoKit)
import CryptoKit
#else
import OpenCrypto
#endif
let digest = try SHA512.hash(data: ...)
A new package integrating Vapor and JWTKit will ship alongside Vapor 4. This package is called JWT and makes it easy to sign and verify JSON Web Tokens from your application:
import JWT
import Vapor
try app.jwt.signers.use(.es512(key: .generate()))
app.post("login") { req -> LoginResponse in
let credentials = try req.content.decode(LoginCredentials.self)
return try LoginResponse(
token: req.jwt.sign(User(name: credentials.name))
)
}
app.get("me") { req -> String in
try req.jwt.verify(as: User.self).name
}
vapor/docker is now home to several useful Docker images for working with Vapor.
These images are for building Vapor projects. They are based on swift
images and include additional dependencies Vapor needs to build, such as OpenSSL and ZLib.
These images are for running compiled Vapor projects. They are based on ubuntu
images with Vapor's additional dependencies installed. These images assume you will copy over Swift's runtime libraries from the build container.