Introduction to Routing in Vapor 3, Part 1

886 Views

  • You must have at least Swift 4.1/Xcode 9.3 installed to complete this tutorial
  • This tutorial does not discuss Futures, but when working with database calls and querying (this tutorial doesn't), you will need to return Futures. Futures will be addressed in a future tutorial (pun intended).

Routing is at the heart of web frameworks, and it's easy with Vapor 3. Let's get started!

First, let's create a new project. Start by running the following command:

    $ vapor new BasicRouting 

Then, change directory into the project with $ cd BasicRouting, and then run $ vapor xcode to generate the Xcode project. Open the Xcode project when prompted.

The Simplest Route

We set up our routes in the routes(_ router: Router) method in routes.swift. All of our routes are created off of our instance of Router. Already set up for us, we can see the first method here:

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

This is the most basic route, which creates a simple get request to /hello. We simply call .get() on our instance of Router, and the closure gives us access to the incoming request.

In such a simple instance, we may not need to explicitly state our return type, but for more complex routes, we need to explicitly state what we expect to return, like this:

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

Every route in vapor has 3 things:

  • a method (get, post, put, patch, delete)
  • a path, supplied by the user ( "hello" in our case)
  • a closure with the request

Also, we may notice that we returned a string. Every route closure in Vapor must return a Type conforming to ResponseEncodable. The most common types conforming to this are:

  • Response - an HTTPResponse object
  • Content - JSON, FormURLEncoded, or Multipart
  • View - a Leaf view

Nesting Routes

A hard coded nested route is quite simple:

    router.get("hi", "how", "are", "you") { request in
        return "this path is /hi/how/are/you"
    }

Parameters

Let's make a more complicated route. Suppose we want to make a request to /cats/, where id is an integer. We could write it like this:

     router.get("cats", Int.parameter) { req -> String in
        let intParam = try req.parameters.next(Int.self)
        return "You have requested route /cats/\(intParam)"
    }

We can very conveniently get the param out with just one line of code, let intParam = try req.parameters.next(Int.self).

So making a request to /cats/4 will yield a result of:

You have requested route /cats/4

Or how about a request to /cats/<id>/<String>?

    router.get("cats", Int.parameter, String.parameter) { req -> String in
        let intParam = try req.parameters.next(Int.self)
        let stringParam = try req.parameters.next(String.self)
        return "You have requested route /cats/\(intParam)/\(stringParam)"
    }

Making a request to /cats/5/grumpycat will yield a result of:

You have requested route /cats/5/grumpycat

Custom Responses

We can also return custom responses in our route handlers. How would we return a customized JSON response, for example?

First, create a struct or class which contains the fields you expect. For example, suppose we want to return a custom JSON like:

    {"name":"anapaix", "apiToken": "kjubeufbfiubui9876bjhbuf"}

We would first create a struct, conform it to Content, and give it the data fields we expect:

    struct UserResponse: Content {
        var name: String
        var apiToken: String
    }

Then, we create our route handler:

    router.get("jsonTest") { req -> Response in // 1
        let response = Response(http: HTTPResponse(status: .ok), using: req) // 2
        let myContent = UserResponse(name: "anapaix", apiToken: "jlhbuoyb987Thgvihtyf") // 3
        try response.content.encode(myContent, as: MediaType.json) // 4
        return response
    }

There's a lot going on here, let's go over it step by step:

  • 1: We declare the route type (get), the path (/jsonTest), and the response type (Response)
  • 2: We create the response object
  • 3: We create a UserResponse object with the data we intend to pass back.
  • 4: we encode our response with the content, and set the MediaType to JSON

For 1, we could create any number of responses, for example:

    Response(http: HTTPResponse(status: .ok), using: req)
    Response(http: HTTPResponse(status: .forbidden), using: req)
    Response(http: HTTPResponse(status: .created), using: req)
    Response(http: HTTPResponse(status: .unauthorized), using: req)

There are many others as well. See the HTTPStatus enum for all options.

Alternatively, if we are just looking to return the data, for name and apiToken, without a custom status code, we could write it as follows:

    router.get("jsonTest2") { req -> UserResponse in 
        return UserResponse(name: "anapaix", apiToken: "jlhbuoyb987Thgvihtyf") 
    }

Query

Suppose we have a route, like /dogs?breed=husky ? How can we get the value from the breed? In the same way that we created a struct to represent the data which we wished to return in our example above, we can create a struct to represent the data we expect to receive here. So if we expect one query parameter, breed, we can create a struct like so:

    struct DogQuery: Content {
       var breed: String
    }

And then, for our query, we can make a get request to /dogs:

    router.get("dogs") { request -> String in
        let dogQuery = try request.query.decode(DogQuery.self)
        return "the breed is \(dogQuery.breed)"
    }

And making a request to /dogs?breed=husky returns:

the breed is husky

So as you can see, once we have decoded the query string, we can access all of its values using simple dot notation. Isn't Vapor 3 wonderful? :D

Alternatively

Also, if you do not wish to create a struct, you can simply use the request.query.get(at: ) method to retrieve the content out of the query string:

    router.get("bangkok") { request -> String in
        let bangkokQuery:String = try request.query.get(at: ["district"]) // 1
        return "the query string here is \(bangkokQuery)"
    }

At 1, you must explicitly declare the type you expect the query string to be. A request to /bangkok?district=siam would yield:

the query string here is siam

Groups

Grouping routes is quite simple in vapor 3. Simply use the group("") method on our instance of router:

    router.group("v1") { v1 in
        v1.get("users") { request in
            return "this route is /v1/users"
        }
    }

We can also combine groups and parameters:

    v1.get("posts", Int.parameter) { request -> String in
         let parameter = try request.parameters.next(Int.self)
         return "You requested /v1/posts/\(parameter)"
    }

Well, that's it for this part 1 in this series! I'll try to keep this updated if anything changes between now and the official Vapor 3 release in mid March. Please leave any questions/comments below!



Enjoy this article? Consider supporting VaporForums by following us on Twitter! Get the latest Vapor articles, tutorials, and news.