The router is getting an overhaul! I started this work back in December, which you can take a look at in the WIP PR. I had two core goals going into this: 1) simplifying the architecture of the router for performance and reliability and 2) adding several new features.
We've reduced the routing code from 604 to 289 LOC; a more than 50% reduction! There are now just two core objects:
Route. Routers contain a name, path, hooks, and any routes that belong within it. Routes are the actual endpoints that are executed when matched.
Most apps will have multiple routers, as we'll see in an example to follow. When processing a request, the app iterates through all the routers until a route is matched and called (read the call method for more details).
Routers can be nested inside of routers, each nested router inheriting the path prefix of it's parent, along with any defined hooks. Nesting makes it really easily to handle things like namespaces:
namespace :foo, "/foo" do
namespace :bar, "/bar" do
The above would create into two
bar being nested within
foo. Because everything (groups, namespaces, etc) is implemented as a
Router object, we guarantee that the same functionality is available no matter how routes are defined. Our previous implementation was lacking in this way and caused a lot of edge cases.
Route objects hold onto a "pipeline", which is a set of blocks (the route block plus any hooks) that should be called in order when the route is matched. All
Route#call does is call each member of the pipeline in order (as seen here). All the pipelines are built at runtime.
The final thing I wanted to point out with the refactor is how everything is frozen after boot, which is something that you'll start to see throughout the framework. We don't want it to be possible to change any app state after boot. This improves both security and performance.
Speaking of performance, the new router is several times faster than the old one. I've been seeing response times consistently less than 400μs (microseconds) in my local environment (running MRI 2.4). It should be noted that this is completely unscientific and only an observation. I'll do a formal benchmark once the refactor is complete.
Okay, enough of that. Let's dive into the new features:
It's now possible to define a router with a name, path, and hooks, effectively creating a group or namespace from the definition itself. For example, these routers are functionally equivalent:
namespace :foo, "/foo", before: [:bar] do
Pakyow::App.router :foo, "/foo", before: [:bar] do
within method lets you easily extend an existing group or namespace.
One use-case for
within is organizing routes. Say your app has an api, but you want to separate the various parts of the api into separate files. Using
within, you can define both the api and non-api version of a resource in a single resource definition:
Pakyow::App.router :api, "/api", before: [:require_api_token] do; end
Pakyow::App.resource :project, "/projects" do
within :api do
We can extend this use-case by adding a nested
comment resource under
Pakyow::App.resource :comment, "/comments" do
within :project do
Request Format Matchers
Format matchers make it possible to specify rules for what content types a route will accept, and execute specific logic based on the content type of the current request. For example:
get "foo.txt" do
get "foo.txt|html" do
req.format :txt do
Better Path Builders
We will have a single lookup method that lets us descend through groups and namespaces. Here's how you would use it to build paths to the routes defined earlier in this post:
router.path(:project, project_id: params[:project_id])
router.path(:api, :project, project_id: params[:project_id])
That's everything on the router list and represents what I'm working on in #211. Are there other routing concerns you'd like to see addressed in v1.0? Ideas for how to improve on the ideas I mentioned above? Chime in!