Episode #350

Vapor Routing

Series: Server-side Swift with Vapor

14 minutes
Published on August 17, 2018

This video is only available to subscribers. Get access to this video and 573 others.

Vapor uses a router to determine how to process incoming requests. In this episode, we will see how to define routes and how to return simple responses. We will see how to return custom JSON responses, how to accept JSON posts, and how to deal with requests with dynamic parameters.

Episode Links

Simple Routes

Matching the root route:

// match /
router.get { req in 
    return "This is the root route"
}

It is important to note that we're returning a String here, but it could be other types of responses as well. It's usually a good idea to help the compiler out and specify the return type explicitly, which can help avoid confusing errors and longer compile times.

// match /
router.get { req -> String in 
    return "This is the root route"
}

Matching a path:

// match /home
router.get("home") { req -> String in 
    return "This is the home page"
}

Returning JSON

To return JSON, we need to create an object that conforms to the Content protocol. This protocol also includes the Codable protocol, so we need to make sure our types conform to that as well.

struct Status : Content {
    var message: String
}

Then we can return an instance of this, and it will get converted for us automatically.

router.get("status") { req -> Status in
    return Status(message: "This is a custom JSON response")
}

If we build and run, then hit this route in the browser, we'll see this response:

{
    "message": "This is a custom JSON response"
}

Pretty easy!

Accepting JSON

We can accept JSON as input with a post request as well. Note that these have the same path, but will be matched based on the HTTP method used as well (GET vs POST).

router.post("status") { req -> String in 
    // try to decode the body
    let status = try request.content.syncDecode(Status.self)
    return status.message
}

Here we use the try keyword, because the body may not be in the format we are expecting. We then call syncDecode, which halts the process to decode the body. Later on we can look at decode, which does this asynchronously.

To test this out, we can use curl from the command line:

$ curl -X POST http://localhost:8080/status -H 'Content-Type: application/json' -d'{"message":"This is a custom post request"}'

It's important to pass the Content-Type header here, otherwise the body will be treated as form encoded, which would look like this: message=this%20is%20my%20custom%20message. This also works, but is less common for APIs. For websites, this is how forms will post data back to the server, and it's handy that Vapor treats both equally. In other words, the client can choose which format and specify via the header, the backend will react accordingly.

For more complex route testing, I recommend using Paw.

Using method handles as routes

Sometimes we want to have 2 routes that have the same logic. For this we can define a method that handles the route:

func handleAboutPage(_ req: Request) -> String {
    return "This is the About page"
}

Then we can reference the method as a paramter in our route definitions:

router.get("about", use: handleAboutPage)
router.get("about_us": handleAboutPage)

Route Parameters

Let's say we want a dynamic part of the route, like this:

/users/ben
/users/mary

To handle these, we need to tell Vapor which part of the route is dynamic.

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

The first line tells Vapor how to match the route. It will match things like /users/foo and /users/123, but won't match /users because there isn't a 2nd route parameter.

Why does it match users/123? Because "123" is a valid string!

If we only wanted to match integers, we could do this:

router.get("users", Int.parameter) { ... }

And then the route would match /users/123 but not /users/ben.

Next we pull the parameter out of the URL. You can think of this as a stack of values, and when you call next(..) you're popping an item off the stack.

Returning Errors

Sometimes we don't have enough input to continue, or we encounter an error we can't recover from. For these we need a way to bail out and return an error response to the client (rather than allowing the server to crash).

Vapor leans on Swift errors and throw for this.

router.get("boom") { req -> String in 
    throw Abort(.internalServerError)
}

There are other types of errors you might want to throw, and this will drive what HTTP status code the client receives.

This episode uses Vapor 3.0.0, Xcode 9.4.1.