Medium-Scale Dancer, Part 2: Route Definitions

By the time a Dancer app grows large enough that you want to start breaking it up into multiple Perl modules, as in the previous article in this series, you've probably also defined enough routes that you're starting to have problems managing them all. Just as with the Perl code, Dancer lets us break up the monolithic route definition set, too.

If you structured your app in the way recommended in the previous article, each major feature of your web app is now in its own Perl module. That Perl module's name likely corresponds to some part of your app's URL scheme. Let's say you're exposing the features of App::MajorFeature as /mf in URLs, with sub-features defined in App::MajorFeature::SubFeature and exposed via /mf/sf routes.

If you extend the generated lib/App.pm file by following the simplest examples from the Dancer documentation, you might have a mess that looks something like this:

get '/mf' => sub {
    # Lots of Perl code to return the top-level MajorFeature view
};

get '/mf/sf' => sub {
    # Implementation of a sub-feature of MajorFeature
};

post '/mf/sf' => sub {
    # Maybe you need a way to add new subfeature objects
};

put '/mf/sf' => sub {
    # And maybe also a way to edit existing subfeature objects
};

del '/mf/sf/:id' => sub {
    # And a way to delete them, too
};

The first thing to fix here is that almost all of the Perl code implementing each route handler should move to lib/App/*.pm. Ideally, each route handler body should do nothing more than call a function in one of these modules:

get  '/mf'        => \&App::MajorFeature::retrieve;
get  '/mf/sf'     => \&App::MajorFeature::SubFeature::retreive;
post '/mf/sf'     => \&App::MajorFeature::SubFeature::add;
put  '/mf/sf'     => \&App::MajorFeature::SubFeature::modify;
del  '/mf/sf/:id' => \&App::MajorFeature::SubFeature::remove;

Notice that by moving all of the code for each route handler into a function in one of our module files, we can replace the inline anonymous function references with explicit fully qualified function references.

There's a fair bit of redundancy in that code, which we'll compress out in two stages.

First, Dancer has the awesome prefix feature, which lets us express the URL hierarchy directly in the code, without repeating elements of the URL:

prefix '/mf' => sub {
    get '' => \&App::MajorFeature::retrieve;
    prefix '/sf' => sub {
        get  ''     => \&App::MajorFeature::SubFeature::retreive;
        post ''     => \&App::MajorFeature::SubFeature::add;
        put  ''     => \&App::MajorFeature::SubFeature::modify;
        del  '/:id' => \&App::MajorFeature::SubFeature::remove;
    };
};

As you can see, this makes the overall code narrower, because we're not repeating ourselves as much. This move toward using prefix will help considerably later in this series of articles.

A second excellent feature of Dancer lets us shorten those lines of code still further.

So far, we've been using explicitly-qualified function names. This is because we want to use short function names within the modules (e.g. retrieve()) without causing namespace collisions by exporting all of the functions. But in fact, there is actually no need to expose the API of your modules outside the module itself. Dancer doesn't care where you define the route handlers, just that they're all defined by the time your caller wants to use them. In the previous part of this article series, we said use App::MajorFeature and such within lib/App.pm, so every one of our app's modules gets executed on startup. This means that any code at global scope within these modules also runs at startup.

Therefore, we can move all of the route definitions above from lib/App.pm to the end of the module where we define each route's handler. That is, the tail end of lib/App/MajorFeature.pm says:

prefix '/mf' => sub {
    get '' => \&retrieve;
};

...and the tail end of lib/App/MajorFeature/SubFeature.pm says:

prefix '/mf' => sub {
    prefix '/sf' => sub {
        get  ''     => \&retreive;
        post ''     => \&add;
        put  ''     => \&modify;
        del  '/:id' => \&remove;
    };
};

The route definitions are much shorter now because they refer to module-local functions defined above the route definitions, so the code references don't need to be fully-qualified.

Having done all this, all that's left behind in lib/App.pm is Dancer startup and other initialization code, such as hook definitions, as is appropriate for the app's top-level module.

The bulk of the application's code is now collected into a set of tightly-scoped modules. These modules should also be largely decoupled since only Dancer needs to know how to call into them, and you told it how from the module itself, by defining routes. Beautiful.

In the next part of this series, we will consider how this application restructuring affects the way we arrange our template files on disk.

Author

This article has been written by Warren Young for the Perl Dancer Advent Calendar 2016.

Copyright

© 2015, 2016 by Educational Technology Resources, licensed under Creative Commons Attribution-ShareAlike 4.0