Vapor 3 makes Codable data a cinch to work with.
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.
SAVE 50% All our books and bundles are half price for Black Friday, so you can take your Swift knowledge further without spending big! Get the Swift Power Pack to build your iOS career faster, get the Swift Platform Pack to builds apps for macOS, watchOS, and beyond, or get the Swift Plus Pack to learn advanced design patterns, testing skills, and more.
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 https://raw.githubusercontent.com/Homebrew/install/master/install)"
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.
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”.
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 req.parameters.next(String.self)
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 req.parameters.next(String.self)
let age = try req.parameters.next(Int.self)
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.
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:
router.post("tweet", String.parameter) { req -> String in
let message = try req.parameters.next(String.self)
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.
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:
router.post(Article.self, 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.
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 req.parameters.next(User.self)
return "Loaded user: \(user.username)"
}
That will respond to “/users/twostraws” with “Loaded user: twostraws”.
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:
router.group("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”:
router.group("article", Int.parameter) { group in
group.get("read") { req -> String in
let num = try req.parameters.next(Int.self)
return "Reading article \(num)"
}
group.get("edit") { req -> String in
let num = try req.parameters.next(Int.self)
return "Editing article \(num)"
}
}
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 req.parameters.next(Int.self)
return "Reading article \(num)"
}
article.get("edit") { req -> String in
let num = try req.parameters.next(Int.self)
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())
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.
SAVE 50% All our books and bundles are half price for Black Friday, so you can take your Swift knowledge further without spending big! Get the Swift Power Pack to build your iOS career faster, get the Swift Platform Pack to builds apps for macOS, watchOS, and beyond, or get the Swift Plus Pack to learn advanced design patterns, testing skills, and more.
Link copied to your pasteboard.