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]
.
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.
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).(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.
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.