Week 3

News

This week, Vapor passed Perfect and has earned the title of “Most Stars of Any Server-side Swift framework on Github”! 🎉🎉🎉🎉 Congrats to the team!

New Features

Helper Methods to Decode Content From Request Bodies

It’s very common to need to take the body of a request and decode it to an object. To cut down on this common boilerplate, new methods were introduced which take care of it automatically with any HTTP method that accepts a body:

router.post(LoginRequest.self, at: "login") { req, loginRequest -> Response in
    print(loginRequest.email) // user@vapor.codes
    print(loginRequest.password) // don't look!
    return req.makeResponse()
}

vapor/vapor#1448

Case Insensitive Routing

While routing is still case-sensitive by default, it is now possible to choose case-insensitive routing instead:

// configure.swift
public func configure(
    _ config: inout Config,
    _ env: inout Environment,
    _ services: inout Services
) throws {
    // Register routes to the router
    let router = EngineRouter(caseInsensitive: true) // or false
    try routes(router)
    services.register(router, as: Router.self)
}

// routes.swift
router.get("hello") { req -> HTTPStatus in
    return .ok
}

Case-sensitive

GET /hello -> 200 OK
GET /Hello -> 404 Not Found

Case-insensitive

GET /hello -> 200 OK
GET /Hello -> 200 OK

Note: The caseInsensitive parameter to EngineRouter() is brand-new and will not be usable until Vapor beta 4.

vapor/engine#209

Value Models

structs can now be used as Fluent Models.

vapor/fluent#372

Community Contributions

Session Redirects

@0xTim introduced middleware to the Auth package that makes it easy to redirect users to another page if they aren’t authenticated. For example, if a user attempts to visit a private page without logging in, you may want to redirect them to /login instead. This replicates the functionality of Vapor 2’s RedirectMiddleware.

vapor/auth#19

Dependency Updates

With the start of tagging, keeping Package.swift files up-to-date is an important but tedious task. If you’re looking to pitch in, but don’t know where to start, plenty of help is needed in this area. There are going to be a lot of these, so we’re going to group them all together:

vapor/fluent-postgresql#13
vapor/leaf#90
vapor/mysql#123
vapor/postgresql#16
vapor/fluent-postgresql#14 vapor/vapor#1498
vapor/mysql#124

Fixed DatabaseConnection Bug

@bensyverson caught that database-kit was making a bad assert (== vs !=)

vapor/database-kit#6

Add Providers to OpenSSL

Adding providers to the ctls package allows SPM to let users know when they’re missing a dependency - @Yasumoto

vapor/ctls#7

Fix Issue Where Large Responses Hang

With larger HTTP bodies, the continueBuffer state was not being handled correctly, causing the last chunk to not be sent and responses to hang. This bug existed in both Vapor 2 and 3. Thanks! @kevinup7

vapor/engine#222

Redundancy Cleanup

@Cellane was able to cut out a couple lines of redundant code. Any cleanup is always appreciated :)

vapor/engine#224

Allow Users of MultipartForm To Set Max Request Size

The maximum request size used to be locked at 1,000,000 bytes, but @bensyverson’s PR allows users to increase this value if they need something larger.

vapor/vapor#1494

Fixes Packet Length and SequenceID

This fix by @SandorDobi should resolve a couple MySQL bugs.

vapor/mysql#122

Add delete(on:) and save(on:) to Future

@twof merged in a couple of convenience extensions on Future that make saving and or deleting a model much cleaner. From

return Reservation
    .query(on: databaseConnectable)
    .filter(\Reservation.user == userName)
    .first()
    .unwrap(or: Abort(.notFound, reason: "No reservations have been made"))
    .map(to: Reservation.self) { reservation in
        return reservation.delete(on: databaseConnectable).transform(to: reservation)
    }.map(to: HTTPStatus.self) { _ in
        return .ok
    }

to

return Reservation
    .query(on: databaseConnectable)
    .filter(\Reservation.user == userName)
    .first()
    .unwrap(or: Abort(.notFound, reason: "No reservations have been made"))
    .delete(on: databaseConnectable)
    .map(to: HTTPStatus.self) { _ in
        return .ok
    }

vapor/fluent#371

Fatal Error Cleanup

As Vapor gets closer to stability, fatal errors need to be removed so that servers aren’t crashing in production. @labradon got the project well on its way by eliminating 29 fatal errors and replacing them with their proper thrown errors. (33 LoC)

vapor/postgresql#15

Update Vapor Toolbox For Most Recent Version of SPM

There were a few deprecated options that made the toolbox unusable for a bit, but all is good again! - @gwynne

vapor/toolbox#210

Improved Error Message

@gwynne updated error messages when hostname and port are misconfigured, which ought to make debugging a bit easier.

vapor/sockets#161

Benchmarks

17k Queries Per Second Per Core Achieved With Fluent/MySQL

That should scale infinitely with the number of cores too. Awesome!

https://twitter.com/JoannisOrlandos/status/964792631875244034

Talks

Joannis @ Swift Amsterdam

In this talk @Joannis will cover the upcoming Vapor 3 release - a complete redesign aimed to be more configurable and performant with less boilerplate. He’ll also talk about dependency inversion, the reactive pattern, Codable, and how these all fit together.

https://swift.amsterdam/talks/serverside-joannis-orlandos.html

Tutorials

Ray Wenderlich Video Course and Upcoming Book by @0xtim

In this 30-video course, you’ll use Vapor, a server-side Swift web framework, to create a complex application that you can communicate with via an API and a website. The first 4 videos are available now!

https://www.raywenderlich.com/186386/new-course-server-side-swift-with-vapor

Pre-orders also opened up for the Ray Wenderlich Vapor 3 book, written by the core Vapor team themselves! This book starts with the basics of web development and introduces the basics of Vapor; it then walks you through creating APIs and web backends, creating and configuring databases, deploying to Heroku, AWS, or Docker, testing your creations, and more!

https://store.raywenderlich.com/products/server-side-swift-with-vapor

How To Set Up a Vapor 3 Project

In this tutorial @martinlasek takes you through all the steps needed to get you from installing Xcode to making requests to your Vapor server.

https://medium.com/@martinlasek/tutorial-how-to-set-up-a-vapor-3-project-75466394cf2e

Microtutorial

This week we’re going to take a look at how to wrap up our .map()s to make promise chains a little more readable. We’ll be continuing from last week’s microtutorial:

router.get("user", UUID.parameter) { (req) -> Future<Response> in // Take in a request
    let userID = try req.parameter(UUID.self) // Turn that request into an ID

    return try User
        .find(userID, on: req) // Use that ID to search the database for a row, turn that row into an (optional) object
        .map(to: User.self) { (user) in // Unwrap the object
            guard let user = user else {throw Abort(.notFound, reason: "User Not Found")}
            return user
        }.map(to: User.self) { (user) in // Make some change to the object
            user.name = "Jamie"
            return user
        }.encode(for: req) // Turn that object into a response, and return that response
}

Let’s see how simple we can make this route! First we’ll conform User to Parameter so we can eliminate the find step.

extension User: Parameter {}

router.get("user", User.parameter) { (req) -> Future<Response> in // Take in a request `/user/<UUID of a User>
    let user = try req.parameter(User.self) // User found with UUID. Aborts if no user is found

    return try user
        .map(to: User.self) { (user) in // Make some change to the object
            user.name = "Jamie"
            return user
        }.encode(for: req) // Turn that object into a response, and return that response
}

With an extension to Future we can remove that last .map()

extension Future where T == User {
    public func changeName(to name: String) -> Future<Expectation> {
        return self.map(to: User.self) { (user) in
            user.name = name
            return user
        }
    }
}

And our final route is quite a bit more readable!

router.get("user", User.parameter) { (req) -> Future<Response> in // Take in a request `/user/<UUID of a User>
    let user = try req.parameter(User.self) // User found with UUID. Aborts if no user is found

    return try user
        .changeName(to: "Jamie") // Make some change to the object
        .encode(for: req) // Turn that object into a response, and return that response
}

Credits

@twof
@calebkleveter
@Cellane
@tanner0101 @gwynne