LAST CHANCE: Save 50% on all my Swift books and bundles! >>

The complete guide to routing with Vapor 3

Vapor 3 makes Codable data a cinch to work with.

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.

Hacking with Swift is sponsored by Essential Developer.

SPONSORED Join a FREE crash course for mid/senior iOS devs who want to achieve an expert level of technical and practical skills – it’s the fast track to being a complete senior developer! Hurry up because it'll be available only until July 28th.

Click to save your free spot now

Sponsor Hacking with Swift and reach the world's largest Swift community!

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"

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
    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
    let age = try
    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:"tweet", String.parameter) { req -> String in
    let message = try
    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:, 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
    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:"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”:"article", Int.parameter) { group in
    group.get("read") { req -> String in
        let num = try
        return "Reading article \(num)"

    group.get("edit") { req -> String in
        let num = try
        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
            return "Reading article \(num)"

        article.get("edit") { req -> String in
            let num = try
            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.

Hacking with Swift is sponsored by Essential Developer.

SPONSORED Join a FREE crash course for mid/senior iOS devs who want to achieve an expert level of technical and practical skills – it’s the fast track to being a complete senior developer! Hurry up because it'll be available only until July 28th.

Click to save your free spot now

Sponsor Hacking with Swift and reach the world's largest Swift community!

Buy Pro Swift Buy Pro SwiftUI Buy Swift Design Patterns Buy Testing Swift Buy Hacking with iOS Buy Swift Coding Challenges Buy Swift on Sundays Volume One Buy Server-Side Swift Buy Advanced iOS Volume One Buy Advanced iOS Volume Two Buy Advanced iOS Volume Three Buy Hacking with watchOS Buy Hacking with tvOS Buy Hacking with macOS Buy Dive Into SpriteKit Buy Swift in Sixty Seconds Buy Objective-C for Swift Developers Buy Beyond Code

Was this page useful? Let us know!

Average rating: 4.0/5

Unknown user

You are not logged in

Log in or create account

Link copied to your pasteboard.