What's New in Vapor 4

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.

New Dependency Injection API

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.

Changing the Default HTTP Port

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

Making Leaf the View Renderer

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)

NIO 2

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.

SSWG

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.

Async HTTP Client

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

New vapor new

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]:

New Model API

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] }

Eager Loading

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" } }, ... ]

Partial Reads & Updates

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.

XCTVapor

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 }

HTTP/2 & TLS

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.

Synchronous Content

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 }

Backpressure

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.

Graceful Shutdown

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.

New Command API

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? } }

APNS

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.

Leaf Syntax

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

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.

OpenCrypto

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: ...)

JWT

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 }

Docker

vapor/docker is now home to several useful Docker images for working with Vapor.

vapor/swift

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.

vapor/ubuntu