What's New in Vapor 4

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. Commands 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

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.

107 Likes

I think the document needs to be improved. :smile: :smile:

8 Likes

There's a lot of great stuff here, amazing work! I'm excited to try it out!

3 Likes

Great work! Very much looking forward to updating my current Vapor 3 applications to use Vapor 4's new features. I always enjoy reading about quality updates to server-side Swift web frameworks; I think the language fits well for this kind of application. I'm especially excited to see how OpenCrypto works out as I have been using CryptoKit for a personal project that also has a Vapor backend.

Glad to see that Vapor is still a healthy open-source project. :+1:

6 Likes

Impressive amount of features :clap: :clap: :clap:

Congratulations to all the Vapor team, now that Kitura is gone we need more projects that keep up with the delivery rate you are always having.

7 Likes

la critique est aisée, mais l’art est difficile

2 Likes

Great work guys! Really eager to test it in production :slight_smile:
Are there any improvements in performance?

3 Likes

I can't speak for the whole framework, you'll probably have to wait for the official TechEmpower benchmarks for that, but we made some significant improvements to the underlying router.

It's a little bit janky at the moment, but we also started performance testing incoming PRs.

Swift NIO also started performance testing PRs in this last cycle.

3 Likes

This is a really important update! Great job! Congratulations Tanner and also to the whole project team!

1 Like

I want to add a timed task to the interface, but I don't know how to do it. I see that the function jobs is provided, can you provide a complete use case?

Vapor 3 was already great but the tooling was a bit lacking.

Vapor 4 with all these nice upgrades is fantastic, and finally spm integration in xcode has made things much nicer overall.

Hopefully the documentation starts taking shape and more people can enjoy it very soon.

2 Likes

Very cool stuff! I am particularly interested in the Jobs API, as there are definitely a few times I would have found such support quite useful. Its good to see this project keep gaining useful additions. Congrats to the whole Vapor team!

Love these features, mostly the config, jobs and the eager loading ones!

Is there a way to change the default primary key to a precise one to lookup data using an alternative unique key when we are loading data by combining parameter member request and a given type ?

final class Galaxy: Model {
@ID(key: "id")
var id: UUID?

//additionnal tag 
@LOOKUP(key: "ref") // works just as an id
var ref: String?

@Field(key: "name")
var name: String
}

/// In the controller route:
route.get(path, Galaxy.parameter, gController.show)

/// in gController.show:
req.parameters.next(Galaxy. **self** ) // will retrieve the data looking up through ref and not the ID.
}

This feature could be usefull to use readable identifiers in the url without bouleplat additioning source code.

Wow~ 2020.2

it's amazing stuff. Thanks, i now prefer you over django

2 Likes

Yesss, all of this work is appreciated. Love the new jobs queuing system and the APNS package! Great work.

In fact to set the port is this code:

app.http.server.configuration.port = 8080
2 Likes
Terms of Service

Privacy Policy

Cookie Policy