Skip to content

Tutorial: Advanced Routing

See the basics

This tutorial continues the project started in Hello World-example.

Trying Changes

This tutorial is going to show-case several ways of defining routes and policies. Feel free to restart the project intermittently by running

sh
hitchy start
hitchy start

and try URLs defined in either case.

Using Controllers

Instead of putting handlers for routes into the router configuration you should start working with controllers.

Note

Controllers are components of a Hitchy-based application.

Create new file api/controllers/hello.js with the following content:

javascript
module.exports = {
    world( req, res ) {
        res.send( "Hello World!" );
    }
};
module.exports = {
    world( req, res ) {
        res.send( "Hello World!" );
    }
};

Open file config/routes.js created as part of previous tutorial and replace its content with the following one:

javascript
exports.routes = {
    "/": ( req, res ) => res.send( "Hello World!" ),
    "/colon": "hello::world",
    "/period": "hello.world",
    "/object": { controller: "hello", method: "world" },
    "/colon/decorated": "HelloController::world",
    "/period/decorated": "HelloController.world",
    "/object/decorated": { controller: "HelloController", method: "world" },
};
exports.routes = {
    "/": ( req, res ) => res.send( "Hello World!" ),
    "/colon": "hello::world",
    "/period": "hello.world",
    "/object": { controller: "hello", method: "world" },
    "/colon/decorated": "HelloController::world",
    "/period/decorated": "HelloController.world",
    "/object/decorated": { controller: "HelloController", method: "world" },
};

This defines different routes resulting in the same output on request. However, all but the first one are using the controller created before. They use different supported ways for addressing a method exposed by a controller.

Neither way of addressing a controller's method should be mistaken as actual code.

Try it!

Restart project and open http://127.0.0.1:3000/object/decorated in your browser now.

Defining Routes per Method

In HTTP every request selects a method, like GET, POST or PUT. This method information can be used for matching routes. The default is GET. You might select any different method by prepending it to the path name of your routing definition:

config/routes.js

javascript
exports.routes = {
    "/": ( req, res ) => res.send( "Hello World!" ),
    "GET /colon": "hello::world",
    "POST /period": "hello.world",
    "DELETE /object": { controller: "hello", method: "world" },
    "PATCH /colon/decorated": "HelloController::world",
    "ALL /period/decorated": "HelloController.world",
    "* /object/decorated": { controller: "HelloController", method: "world" },
};
exports.routes = {
    "/": ( req, res ) => res.send( "Hello World!" ),
    "GET /colon": "hello::world",
    "POST /period": "hello.world",
    "DELETE /object": { controller: "hello", method: "world" },
    "PATCH /colon/decorated": "HelloController::world",
    "ALL /period/decorated": "HelloController.world",
    "* /object/decorated": { controller: "HelloController", method: "world" },
};

The last two cases show special keywords ALL and * supported as special method names for defining routes applying to every request without regard to its HTTP method.

Try it!

In opposition to the previous case, some routes can't be tested in browser that simply, anymore. Testing those routes bound to methods POST, DELETE and PATCH result in showing Page not found error.

What about Policies?

Policies are very similar to controllers as described before.

Create a file api/policies/filter.js with the following content:

javascript
module.exports = {
    failOnDemand( req, res, next ) {
        if ( req.query.fail ) {
            this.api.log( "hitchy:request" )( "responding on failure" );
            res.status( 400 ).send( "Failed!" );
        } else {
            next();
        }
    }
};
module.exports = {
    failOnDemand( req, res, next ) {
        if ( req.query.fail ) {
            this.api.log( "hitchy:request" )( "responding on failure" );
            res.status( 400 ).send( "Failed!" );
        } else {
            next();
        }
    }
};

Remarks

A special Hitchy API is exposed as this.api in every request handler. This API includes a logging service which is used here.

Next create a file config/policies.js with the following content:

javascript
exports.policies = {
    "/period": "filter.failOnDemand",
    "/object/decorated": "FilterPolicy::failOnDemand",
};
exports.policies = {
    "/period": "filter.failOnDemand",
    "/object/decorated": "FilterPolicy::failOnDemand",
};

This is associating incoming requests with your policies. It is very similar to defining routes to controllers and thus it is called policy routing. Of course, you could also use those other styles of addressing a target demonstrated on controller routes before.

Major differences between policies and controllers are:

  • The fully decorated version of a policy component uses suffix Policy instead of Controller.
  • The routing configuration is exposed via exports.policies instead of exports.routes.
  • As described in routing basics, policies apply to all requests matching prefix of request path name.

Try It!

Restart project and open http://127.0.0.1:3000/object/decorated?fail=1 in your browser now. It will show Failed! instead of Hello World!. By removing the ?fail=1 the greeting returns. In addition, there is an output in console used to run Hitchy.

About Routing Slots

In routing basics you've learned about routing slots. In your application's configuration you can split up your definition of routes and policies to apply to separate slots of routing:

In file config/routes.js or config/policies.js you can replace configurations like these

javascript
exports.policies = {
    "/period": "filter.failOnDemand",
    "/object/decorated": "FilterPolicy::failOnDemand",
};
exports.policies = {
    "/period": "filter.failOnDemand",
    "/object/decorated": "FilterPolicy::failOnDemand",
};

with grouped configurations like

javascript
exports.policies = {
    before: {
        "/period": "filter.failOnDemand",
    },
    late: {
        "/object/decorated": "FilterPolicy::failOnDemand",
    },
};
exports.policies = {
    before: {
        "/period": "filter.failOnDemand",
    },
    late: {
        "/object/decorated": "FilterPolicy::failOnDemand",
    },
};

This would apply the previously declared policy routings into separate slots of separate routing stages.

Try it!

By applying the latter policy to the late slot the URL http://127.0.0.1:3000/object/decorated?fail=1 wouldn't result in displaying Failed! this time. However, the policy is used nonetheless, as you can see by the log message in Hitchy's output.

Routing Parameters

Hitchy depends on popular path-to-regexp to support highly flexible definition of request paths. This includes definition of named parameters to be exposed in context of request handlers.

Append the following route definition in config/routes.js. This time you should know how to achieve that.

javascript
"/greet/:clientName": "Hello::world"
"/greet/:clientName": "Hello::world"

In api/controllers/hello.js replace existing method world with this one:

javascript
world( req, res ) {
    res.send( `Hello ${req.params.clientName || "world"}!` );
}
world( req, res ) {
    res.send( `Hello ${req.params.clientName || "world"}!` );
}

Try it!

After restarting the response gets personal when requesting URL like http://127.0.0.1:3000/greet/John in your browser.