Getting Started With Vapor 3 Part 3:

Basic Routing

Posted 7/28/2018.

In Part 2 we explored the basic structure of a Vapor app. Next we'll look at how to declare simple routes with dynamic path components and query parameters.

Our API will model information about U.S. national parks, so we'll start by declaring a simple Park model. For now, we'll keep an array of parks in memory and define two routes. The first will demonstrate dynamic path components and return a park by ID with the path /parks/:id. The second will use query parameters to search for a park by name with the path /parks/search?name=[name].

Routing

Note: This section breaks down how routing works in Vapor. If you just want to see examples of routing in practice and want to return to the details of the protocols that make this work later, feel free to skip to the "Examples" section below.

HTTP Methods

Before we declare the park routes, let's review the basics of Vapor routing.

We've already seen a simple GET request in Part 2, where we returned the String "Hello, world!" for the path /hello:

public func routes(_ router: Router) throws {
    router.get("hello", use: sayHelloWorld)
}

private func sayHelloWorld(_ request: Request) throws -> String {
    return "Hello, world!"
}

A Vapor Router has five functions which map to the five possible HTTP methods: GET, PUT, POST, PATCH, and DELETE. They all have the same signature. Let's take a look at the function for a GET request:

public func get<T>(_ path: PathComponentsRepresentable..., use closure: @escaping (Request) throws -> T) -> Route<Responder>
        where T: ResponseEncodable
{
    return _on(.GET, at: path.convertToPathComponents(), use: closure)
}

There's a lot to break down here.

  1. The path is defined as variadic parameters conforming to PathComponentsRepresentable. In Vapor, conforming types include String, PathComponent, and an array whose elements conform to the protocol. PathComponent is an enum which will be used to contain dynamic parameters in the path (more on that below).
  2. The HTTP method functions are generic and take a closure of type (Request) -> T. There is a generic constraint that T conform to ResponseEncodable, a protocol which allows conforming types to define how they are converted into a displayable response.

Knowing this, let's look at a few ways we can represent the same route:

public func routes(_ router: Router) throws {
    router.get("this/is/a/long/path", use: longPath)
    router.get("this", "is", "a", "long", "path", use: longPath)
    let components: [PathComponentsRepresentable] = ["this", "is", "a", "long", "path"]
    router.get(components, use: longPath)
}

private func longPath(_ request: Request) throws -> String {
    return "This is a long path."
}

In the first example, we're using a single string to define the path. In the second, we pass in each path component as a separate parameter. In the third, we pass in an array of path components. All three define the same route. Passing each component as a separate parameter (the second example) is recommended for readability.

The closure passed in is a basic function that takes a Request and returns a String, which in Vapor already conforms to ResponseEncodable.

Dynamic Path Components

Next, let's look at how to define dynamic path components. Vapor defines a protocol Parameter that has a static property parameter of type PathComponent. We saw above that a PathComponent conforms to PathComponentsRepresentable and therefore can be passed into the HTTP methods. String and the numeric types (e.g. Int, Double, Float) all conform to Parameter.

Using Parameter, we can define a route with a dynamic path component as follows:

public func routes(_ router: Router) throws {
    router.get("hello", String.parameter, use: helloName)
}

private func helloName(_ request: Request) throws -> String {
    let name = try request.parameters.next(String.self)
    return "Hello, \(name)!"
}

The next function on a request's parameters takes a type conforming to Parameter, so we can specify which type we expect in the route. Try this by entering the path /hello/[YOUR_NAME] in your browser.

Query Parameters

Finally, let's see how to work with query parameters.

public func routes(_ router: Router) throws {
    router.get("search", use: search)
}

private func search(_ request: Request) throws -> String {
    let query = try request.query.get(String.self, at: "q")
    return "You searched for \(query)."
}

The get function on a request's query allows us to specify what type we expect for a given key. Try this by entering the path /search?q=[SEARCH_TERM] in your browser.

Examples

We're now ready to define the park routes described in the introduction. First, let's declare our basic Park model. At the top of routes.swift, add the following class:

final class Park {

    let id: Int
    let name: String
    let established: Int

    init(id: Int, name: String, established: Int) {
        self.id = id
        self.name = name
        self.established = established
    }

}

Now we'll add two instances we can return in our routes:

let acadia = Park(id: 1, name: "Acadia", established: 1919)
let yosemite = Park(id: 2, name: "Yosemite", established: 1890)
let parks = [acadia, yosemite]

Now replace the rest of this file with the following:

public func routes(_ router: Router) throws {
    router.get("parks", Int.parameter, use: show)
    router.get("parks", "search", use: search)
}

private func show(_ request: Request) throws -> String {
    let id = try request.parameters.next(Int.self)
    guard let park = parks.filter({ $0.id == id }).first else {
        throw Abort(.notFound, reason: "No Park was found with ID '\(id)'")
    }
    return "\(park.name): Established in \(park.established)."
}

private func search(_ request: Request) throws -> String {
    let query = try request.query.get(String.self, at: "name").capitalized
    guard let park = parks.filter({ $0.name == query }).first else {
        throw Abort(.notFound, reason: "No Park was found with name '\(query)'")
    }
    return "\(park.name): Established in \(park.established)."
}

In the show route, we first get the ":id" path component and check that our parks array contains an instance with that ID. If not, we throw an error. Otherwise, we return a string describing the park. Similarly in the search route, we get the "name" query parameter, check if there is a park with that name, and return the appropriate error or description.


In this part, we saw how routing works in Vapor and how to support dynamic path components and query parameters. We then started to define routes for what could become a Park controller. So far, we've used in an in-memory array as the data store for the park routes. Next, we'll explore how to use PostgreSQL with Vapor.