NEW: Learn SwiftUI with my free YouTube video series! >>

The complete guide to routing with Vapor 3

Paul Hudson       @twostraws

Routing takes a URL such as /messages/twostraws and executes the appropriate code for that path, sending back the results to the user. It’s really the fundamental building block of any web application, so it won’t surprise you to learn it can do a lot depending on your needs.

In this article we’re going to set up an example web project using Vapor, then experiment with various ways of handling routes. We’re going to start off with the basics for folks who haven’t tried Vapor before, but quickly ramp up to more advanced solutions that deliver the kind of power and flexibility real sites demand.

Setting up our sandbox

Before we can start with any code, we need to install the Vapor toolkit. This lets us build, test, and deploy Vapor projects easily, and it’s available through Homebrew.

So, first you need to install Homebrew. If you run “brew” from the command line and see “command not found” then it means you don’t have it installed and should run this command:

/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"

Note: that will ask for your password.

Once Homebrew is ready, you can install the Vapor toolkit using this command:

brew install vapor/tap/vapor

You can check that the toolkit is installed by running vapor --help – you should get some useful information back.

Now that you have the Vapor toolbox ready, we can create an empty Vapor project that we can use for experimenting. Run this command:

vapor new RouteTest --template=twostraws/vapor-clean
cd RouteTest

That creates a new Vapor project called RouteTest, asking it to clone my example project from GitHub. Don’t worry: that example project contains the absolute least amount of code required to set up a Vapor server – it’s so bare bones some folks have tried to wear it as a Halloween costume.

So, we installed Homebrew, which lets us install third-party software easily onto macOS. We then installed the Vapor toolbox, which is a set of tools to make the life of Vapor developers easier. And now we’ve created a new Vapor project called RouteTest, giving it just enough code to run a simple server.

The final set up step is to create an Xcode project for RouteTest. This is done using a Vapor toolbox command: vapor xcode. When you run that, Vapor will download all the dependencies to make our app work (the code for Vapor itself), then generate an Xcode project we can work with. The Xcode part isn’t required – you can work in Emacs if you want – but it does mean you get to benefit from all the features of Xcode, such as syntax highlighting and code completion.

When Xcode launches, go to the Product menu then choose Scheme > Run, and press Cmd+R to build and run the app. After a few seconds you should see in the Xcode log, signaling that the server is up and running. So, launch your web browser and visit http://localhost:8080/hello – you should see “Hello, world!”, which signals that the server is up and running.

Basic routes

Look in the project navigator for the file routes.swift – you’ll find it inside the Sources > App group. Inside there you will see the default Vapor route was included in my template:

router.get("hello") { req in
    return "Hello, world!"
}

That tells us it responds to GET requests to “/hello” by returning the string “Hello, world!”. The req parameter passed into our closure contains information about the user’s request – any parameters they passed, for example.

If we wanted to read multiple directory components, we stack them up like this:

router.get("hello", "world") { req in
    return "Hello, world!"
}

That will now respond to requests for “hello/world”.

Reading parameters

You can replace any string part of the route with a Swift type. For example, use String.parameter to specify that any sort of string might come at a specific point in the path:

router.get("greet", String.parameter) { req -> String in
    let name = try req.parameters.next(String.self)
    return "Hello, \(name)!"
}

That path will respond to requests for “/greetings/Taylor” by printing out “Greetings, Taylor!”.

You can also use doubles and integers of various sizes. For example, we might write a route that takes someone’s name and age, like this:

router.get("greet", String.parameter, Int.parameter) { req -> String in
    let name = try req.parameters.next(String.self)
    let age = try req.parameters.next(Int.self)
    return "Hello, \(name)! You're \(age)."
}

That would respond to “/greet/Taylor/26”.

Tip: String can actually match anything in the URL if you want it to – ultimately your whole URL is just a string, so even though it contains numbers you can still match them as strings if you want. That is, the URL “/1/2/3” could be matched using String.parameter rather than Int.parameter if you wanted.

Other HTTP methods

You can attach routes to any HTTP method just by changing your method call: delete(), patch(), post(), and put() are all available.

For example, to read something submitted using a HTTP POST method you might write this:

router.post("tweet", String.parameter) { req -> String in
    let message = try req.parameters.next(String.self)
    return "Posting \(message)..."
}

Broadly speaking, on the web POST methods should be used for making writes and GET should be used for making reads. If you’re not working on the web you can also use DELETE, PUT, and so on.

Decoding objects from a form

Vapor has helper methods that handle decoding objects as part of a URL, and it’s a smart idea to use them whenever possible.

To use the method, first make some sort of struct that conforms to Content, like this:

struct Article: Content {
    var title: String
    var content: String
}

Once you have that, you use a slightly different routing method that has your type as its first parameter, followed by at and the rest of your path. This time, though, the route closure takes two parameters: the request that came in, plus the decoded object.

For example:

router.post(Article.self, at: "article") { req, article -> String in
    return "Loaded article: \(article.title)"
}

You can try that out by running Curl from the command line:

curl -H "content-type: application/json" -X POST localhost:8080/article -d '{"title": "Swift 5.0 rocks", "content": "Oh yes it does."}'

Our route will run only if Vapor could successfully decode an instance of Article from the JSON.

Custom parameter types

As well as using Swift’s own types as parameters, you can also create custom types and load them directly in URLs. To do this, conform your type to Parameter and add a resolveParameter() method that accepts a parameter string and returns an instance of your type.

For example, here’s a User struct:

struct User: Parameter {
    var username: String

    static func resolveParameter(_ parameter: String, on container: Container) throws -> User {
        return User(username: parameter)
    }
}

In practice, your own custom structs will do some sort of database lookup before returning the object, but you get the idea.

Once you have your custom type ready, you can create routes like this:

router.get("users", User.parameter) { req -> String in
    let user = try req.parameters.next(User.self)
    return "Loaded user: \(user.username)"
}

That will respond to “/users/twostraws” with “Loaded user: twostraws”.

Creating groups

The group() method lets us group routes together under a specific part of the URL, such as “/admin”.

For example, if we wanted “/admin/edit” and “/admin/new” we could create a group like this:

router.group("admin") { group in
    group.get("edit") { req in
        return "Edit article."
    }

    group.get("new") { req in
        return "New article."
    }
}

You can attach parameters anywhere the main route or the inner routes, but you should only try to read them inside the inner routes. For example, this will connect both “/article/1/read” and “/article/1/edit”:

router.group("article", Int.parameter) { group in
    group.get("read") { req -> String in
        let num = try req.parameters.next(Int.self)
        return "Reading article \(num)"
    }

    group.get("edit") { req -> String in
        let num = try req.parameters.next(Int.self)
        return "Editing article \(num)"
    }
}

Route collections

Alongside route groups, you can also create subrouters using the RouteCollection protocol. We’ve been using the routes() function in routes.swift for all our tests, but you can also create subrouters and pass work to them as needed.

For example, we could create a route collection to handle the admin section of a site:

struct AdminCollection: RouteCollection {
    func boot(router: Router) throws {
        let article = router.grouped("article", Int.parameter)

        article.get("read") { req -> String in
            let num = try req.parameters.next(Int.self)
            return "Reading article \(num)"
        }

        article.get("edit") { req -> String in
            let num = try req.parameters.next(Int.self)
            return "Editing article \(num)"
        }
    }
}

That new boot() method is the same as the routes() function we’ve been used so far. We can then connect this route collection to our main router by registering that collection: inside the routes() function of routes.swift:

try router.register(collection: AdminCollection())

Where next?

Vapor’s routing system is extremely powerful, and does a really great job of giving us control over data coming in and out.

We’ve only looked at the routes part here, but in my Server-side Swift book we go into lots more detail about returning different kinds of data, database access, sessions, and more – it’s so much fun to work with, and the beautifully Swifty API makes it surprisingly easy to use.

SPONSOR Meet the new Instabug – more than just bug reporting! We help you build better apps and minimize your debugging time. With each bug report, we automatically capture details like network requests, repro steps, and session details. Get real-time crash reports with stack trace details and session data to help you catch and fix issues easily. And with our customizable in-app surveys, you’ll gather insightful user feedback and much more. Instabug is the fastest and easiest way to release with confidence. Start your free trial now! Start your free trial now!

 

MASTER SWIFT NOW
Buy Testing Swift Buy Practical iOS 12 Buy Pro Swift Buy Swift Design Patterns Buy Swift Coding Challenges Buy Server-Side Swift (Vapor Edition) Buy Server-Side Swift (Kitura Edition) Buy Hacking with macOS Buy Advanced iOS Volume One Buy Advanced iOS Volume Two Buy Hacking with watchOS Buy Hacking with tvOS Buy Hacking with Swift Buy Dive Into SpriteKit Buy Swift in Sixty Seconds Buy Objective-C for Swift Developers Buy Beyond Code

About the author

Paul Hudson is the creator of Hacking with Swift, the most comprehensive series of Swift books in the world. He's also the editor of Swift Developer News, the maintainer of the Swift Knowledge Base, and Mario Kart world champion. OK, so that last part isn't true. If you're curious you can learn more here.

Was this page useful? Let me know!

Average rating: 5.0/5

Click here to visit the Hacking with Swift store >>