[dancer-users] Advent 2015, medium-scale Dancer, part 2: Route Definitions

Warren Young wyml at etr-usa.com
Tue Oct 27 01:14:22 GMT 2015


FYI: Don’t judge the code in the following article part too harshly.  It’s all been written off-the-cuff.  I have not yet tried to save it out into a final app and make sure it runs.  That will be part of the polishing passes to come.  The main thing I want comment on is the overall ideas presented, not the niggly details.

I don’t mean that I do not welcome nit-picking, just that I prefer that you keep it brief.  Yes, I realize that the code probably doesn’t work as-is yet, and yes, I realize that I probably have committed spelling and grammar mistakes.  Kindly just point them out, without discussing how this pile of errors reflects on my ancestry. :)



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 first part of this series, each major feature of your web app is 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 underneath that.

If you extend the generated `lib/App.pm` file in the most obvious way, using 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/subfeature' => sub {
        # Implementation of a sub-feature of MajorFeature
    };
    post '/mf/subfeature' => sub {
        # Maybe you need a way to add new subfeature objects
    };
    put '/mf/subfeature' => sub {
        # And maybe also a way to edit existing subfeature objects
    };
    del '/mf/subfeature/: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'                => sub { App::MajorFeature::get(context); };
    get  '/mf/subfeature'     => sub { App::MajorFeature::sub_feature(context); };
    post '/mf/subfeature'     => sub { App::MajorFeature::add(context); };
    put  '/mf/subfeature'     => sub { App::MajorFeature::modify(context); };
    del  '/mf/subfeature/:id' => sub { App::MajorFeature::remove(context); };
    
The `context()` function is a small wrapper I define at global scope within the app:

    sub context {
        return {
            config  => config(),
            request => request(),
            session => session(),
            conn    => App::Utility::get_db_conn(),
            etc     => ...
        };
    }
    
That is, it just bundles up a bunch of Dancer objects into a hash for you. I find this convenient, but perhaps you will want to use the DSL instead.

The URL scheme defined above is quite redundant. We can factor out that redundancy in two stages.

First, Dancer has the awesome [`prefix`](https://metacpan.org/pod/distribution/Dancer2/lib/Dancer2/Manual.pod#prefix) feature, which lets us express the URL hierarchy directly in the code, without repeating each element:

    prefix '/mf' => sub {
        get '/' => sub { App::MajorFeature::get(context); };
        prefix '/subfeature' => sub {
            get  '/'    => sub { App::MajorFeature::sub_feature(context); };
            post '/'    => sub { App::MajorFeature::add(context); };
            put  '/'    => sub { App::MajorFeature::modify(context); };
            del  '/:id' => sub { App::MajorFeature::remove(context); };
        };
    };

Factoring out the `/mf` part was a net loss of 1 character per route with our 4-space indents, but factoring out `/subfeature` saved us a net 6 characters per route, which really helps make the code easier to read.

You may find after rewriting your URL handlers this way that you see structural patterns in the URLs that will lead you toward a better URL design. This can be especially helpful in REST API design, which we will discuss in a later part of this article series.

But to get back to our refactoring work, 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. `Get()`) 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 `lib/App/MajorFeature.pm`:

    prefix '/mf' => sub {
        get '/' => sub { get(context); };
        prefix '/subfeature' => sub {
            get  '/'    => sub { sub_feature(context); };
            post '/'    => sub { add(context); };
            put  '/'    => sub { modify(context); };
            del  '/:id' => sub { remove(context); };
        };
    };

Now all the function calls are made within the module itself, so we don't need to qualify them.

If you are using my `context()` idea, you will have to move it out of `lib/App.pm`, such as into the `App::Utility` module, exported by default:

    package App::Utility {
        use Dancer2 appname => 'App';
     
        require Exporter;
        use base qw(Exporter);
        our @EXPORT = qw(context);

        sub get_db_conn {
            # do something useful here;
            # not exported, since context->{conn} holds our return value
        }
   
        sub context {
            return {
                config  => config(),
                request => request(),
                session => session(),
                conn    => get_db_conn(),
                etc     => ...
            };
        }
    }
    
In the first part of this article series, we moved almost all of the Perl code from `lib/App.pm` into a collection of tightly-scoped `lib/App/*.pm` modules. In this second part, we moved most of the route definitions into those modules, too, not only cleaning up `lib/App.pm`, but also shortening the definition of those route handlers considerably by removing redundant code. All that should be left in `lib/App.pm` is app-wide Dancer setup code, such has global hook definitions.

In the next part, we will consider how this application restructuring affects the design of other parts of the web app.


More information about the dancer-users mailing list