This package has been deprecated

Author message:

Package no longer supported. Use at your own risk

fsroute

0.4.0 • Public • Published

fsroute Build Status

FSRoute is an Express-compatible middleware router that serves resources from a tree structure and/or filesystem paths that correspond to the URL.

Getting Started

Install the module with: npm install fsroute

var FSRoute = require( 'fsroute' );

Documentation

FSRoute is an Express-compatible middleware router that serves resources from a tree structure and/or filesystem paths that correspond to the URL.

Filesystem modules

Given a URL of http://example.com/foo/bar and a resource root directory of /root-directory, a module at /root-directory/foo/bar.js would serve the request. The function that is exported from that module would be called when a request for that URL was received.

The root of the tree is specified by first creating an FSRoute object and then invoking its add_modules method, passing the absolute path of the root directory.

Tree of objects

Alternatively, a number of serving functions could be gathered together in a tree of javascript objects. In this case, the above request would be served from the following tree:

{
    foo: {
        bar: function(descend) {
        }
    }
}

Such a tree would likely, in practice, include more than one function and a more complex tree structure. If both a tree and a root directory are specified, the two are merged.

The tree of objects is passed to the FSRoute constructor.

Clustered objects

Instead of exporting a function, filesystem modules may export an object. When FSRoute encounters an exported object, it will iterate through the object's keys. If the associated value is a function, the key will be appended to the module's path and treated as if a module was found at the resulting extended path.

So given /root-directory/foo.js:

module.exports= {
  a: function() {
    this.res.send('responds to /fooa')
  },
  '': function() {
    this.res.send('special case: empty string as key, responds to /foo')
  },
  '/': function() {
    this.res.send('special case: responds to /foo/')
  },
  '/bar': function() {
    this.res.send('responds to /foo/bar')
  },
  '/*': function(descend) {
    this.res.send('special case: called with all requests in /foo/*')
    descend()
  }
}

If the associated object is also an object, rather than a function, its keys specify HTTP methods and the function associated with the method handles requests of that method: So if we replace '/bar' above with:

  '/bar': {
    GET: function() {
        this.res.send('responds to GET /foo/bar')
      },
    POST: function() {
        this.res.send('responds to POST to /foo/bar')
      },
    PUT: function() {
        this.res.send('responds to PUTS to /foo/bar')
      }
    }

Mix and match

A mixture of tree objects and filesystem modules (some or all of which may contain clustered objects) is allowed. Find a mix that works for you.

API


Request handler functions

The functions in the routing tree and exported from modules in the filesystem resource directory receive one argument, descend and are called in a this context that is shared by all handlers for a given request. The context should include a req request object and a res response object. The router will add next to the context, allowing exit from the router to the next middleware layer.

The this context is shared among all handlers serving a given request. It can serve as a bus, converying objects or settings from one handler to another. If FSRoute is used as middleware in composable middleware, this will be shared among all that middleware as well. The common bus can thus extend through several layers of middleware, including one or more FSRoute routing layers.

The descend method moves to the next step in FSRoute, whereas this.next() goes to the next middleware layer.

If the path is indeterminate, this.remainder will be everything in the URL to the right of the handler's path. If the handler is for /foo/ and the URL is /foo/a/b/c, this.remainder will be a/b/c.

If you name your handler function, the handler's path can be found in [function-name].path. Requesting /foo/bar from the following tree will result in a reply of /foo/bar:

{foo:
  {
    bar:function fn(){
      this.res.send(fn.path)
    }
  }
}

A request handler may also have the signature of Connect middleware: function(request,response,descend).

Request handlers must either:

  • Send a response.
  • Call the descend() callback to pass the request on to the next handler.
  • Call this.next() to signal a not found condition and pass the request to the next middleware layer.
  • Call descend(err) or this.next(err) to signal an error.

var fsRoute= new FSRoute(tree)

Creates a new router.

The tree argument is optional. If present, it should be an object containing functions and embedded objects that define the routing to be done. For example {foo:{bar:function() {}}} would define a router that directs requests for /foo/bar to the given function. Read on for more examples.


fsRoute.add_modules(rootdir)

Adds the modules in the directory rootdir and its subdirectories to the router.

Returns the fsRoute object for which is was invoked, allowing chaining.


fsRoute.request_handler(context,next)

Handle one request. The request_handler is called once for each request to be routed. The request will be routed to the function or functions from the tree or modules that are to handle the request. The request_handler function is asynchronous.

The handler functions will be called in the context of the context object. It will be this in any handler function. FSRoute expects it to include a req request object and a res response object, but FSRoute does not require that. The request object must include a method and a url.

If no functions are defined to handle the request, or if they all pass the request on to their descend callback, the next function will be called.

The request_handler may be called within a handler function. This allows support of internal redirection and partials. A The request handler of a different instance of FSRoute can also be called, allowing complex embedded structures.


fsRoute.connect_middleware()

Convenience method for calling request_handler in Connect compatible middleware.

Returns a middleware function that calls request_handler.


fsRoute.composable_middleware()

Convenience method for calling request_handler in composable middleware. The composable middleware this context will be shared context for all request handler functions.

Returns a middleware function that calls request_handler.


fsRoute.set_module_extentions(extensions)

Defines the file extensions that will be recognized as modules. The default is js and coffee. The argument(s) should be strings without a leading dot.

Returns the fsRoute object for which is was invoked, allowing chaining.


fsRoute.set_determinate_handler_caller(caller)

This method allows adding a hook that is called whenever a determinate request handler function is called. Any given request may result in calls to several request handler functions: one for each indeterminate step and finally, if the path is determinate, one for the determinate handler. The caller will be called when the determinate handler is to be called. One reason one might add such a hook is to allow promise-based request handlers.

The default handler_caller is:

function(handler,context,descend) {
  return handler.call(context,descend)
}

fsRoute.set_indeterminate_handler_caller(caller)

This method is very much like set_determinate_handler_caller except that it is called whenever an indeterminate request handler is to be called. The defaults are identical.


this.on_no_determinate

If an indeterminate request handler assigns a function to this.on_no_determinate, that function will be called whenever a request is received for which there is not a determinate handler.

Given the following:

      {foo:
        {
          '*': function(descend) {
            this.on_no_determinate= function() {
              this.res.send('No determinate handler')
            }
            descend()
          },
          bar:function fn(req,res,next){
            ...
          }
        },
        }
      }
  • A request for /foo/bar would be handled by the foo/bar handler
  • Since there is no handler defined for foo/qux, the on_no_determinate function would be called.

on_no_determinate allows checking first for functions or modules to serve the request, and failing that, looking for resources, such as in the filesystem or in a database, to satisfy the request.


this.add_slash_to_directory

If this.add_slash_to_directory is set to a truthy value, the URL is not found and adding a slash to the URL would find a handler for the request, a redirect to that slashed URL will result.

Given the following tree:

{
  foo: {
    '/': function() {...}
    '*': function() (
      this.add_slash_to_directory= true;
      descend();
    }
  }
}

There is no handler for a URL of /foo. But since there is a handler for /foo/ and this.add_slash_to_directory is true because it was set in the foo directory default handler, a redirect to /foo/ will result.


this.fsRoute

The FSRoute object serving the request. Most notably, this.fsRoute can be called to effect internal redirection or to load a partial.


this.parsed_url

The request's URL as returned from Node's url.parse.


#### this.path_in(dir)

Given the path to a directory, returns the path of the URL in that directory. For example, given a URL of /foo/bar, this.path_in('/my/directory') returns /my/directory/foo/bar


Determinate and Indeterminate paths

Most paths are determinate. Requesting http://example.com/foo/bar results in the module at /root-directory/foo/bar being run.

It is also possible to define indeterminate path handlers. If a module is defined at /root-directory/foo/_DEFAULT.js or in a tree like {foo:{'*':fn()}} all requests starting http://example.com/foo/, including http://example.com/foo/bar or http://example.com/foo/abc/def/ghi, even if not explicitly defined as a determinate path will pass through that handler.

If indeterminate handlers are defined for both http://example.com/ and http://example.com/foo/ as well as a determinate handler for http://example.com/foo/bar, the request will first be handled by the root directory handler, then by the foo directory handler before finally being handled by the determinate http://example.com/foo/bar handler. This stack of handlers works as middleware. Each is called with a descend callback. The request only reaches the next handler if descend is called. See the Directory default handlers section and the Virtual directories section for more information.

In addition to the obvious determinate http://example.com/foo/bar path, there are a couple of special case definitions. A determinate handler can be defined for http://example.com/foo/ or for http://example.com/foo for cases where foo is both a directory and a specific resource. Determinate handlers may also be defined for URLs with extensions, such as http://example.com/foo/bar.css. Determinate handlers only serve GET and HEAD requests unless they are defined to be specific to a given HTTP method.

URL routing guide

In the table below, fn() is an abbreviation for function(descend){}. The descend argument is optional. The function must either send a response to this.res, call descend() to descend to the next route handler or call next() to punt the request to the next middleware layer.

URL file path tree notes
/foo/bar foo/bar.js {foo:{bar:fn()}} [1]
POST /foo/bar foo/bar._POST.js {foo:{'bar._POST':fn()}} [2]
/foo/bar.css foo/bar.css.js {foo:{'bar.css':fn()}} [3]
POST /foo/bar.css foo/bar.css._POST.js {foo:{'bar.css._POST':fn()}} [3] [2]
/foo foo.js {'foo.':fn(),foo:{...}} [4]
POST /foo foo._POST.js {'foo._POST':fn(),foo:{...}} [4] [2]
/foo/ foo/_INDEX.js {foo:{'/':fn()}} [5]
POST /foo/ foo/_INDEX._POST.js {foo:{'/._POST':fn()} [5] [2]
/foo/... foo/_DEFAULT.js {foo:{'*':fn()}} [6]

Here is a tree that puts this all together:

{
  '*':fn(),            // all requests http://example.com/... [6]
  'foo.':fn(),         // GET http://example.com/foo [4]
  'foo._POST':fn(),    // POST http://example.com/foo [4][2]
  foo:{
    '*':fn(),          // all requests http://example.com/foo/... [6]
    '/':fn(),          // GET http://example.com/foo/ [5]
    '/._POST':fn(),    // POST http://example.com/foo/ [5]
    'bar._POST': fn(), // POST http://example.com/foo.bar [1][2]
    bar: fn(),         // GET http://example.com/foo.bar [1]
    'bar.json._POST': fn(), // POST http://example.com/foo.bar.json [3][2]
    'bar.json': fn()   // GET http://example.com/foo.bar.json [3]
  }
}

The same site implemented in individual files:

/root-dir/_DEFAULT.js   (all requests http://example.com/... [6])
/root-dir/foo.js        (GET http://example.com/foo [4])
/root-dir/foo._POST.js  (POST http://example.com/foo [4][2])
/root-dir/foo/_INDEX.js (GET http://example.com/foo/ [5])
/root-dir/foo/_DEFAULT.js  (all requests http://example.com/foo/... [6])
/root-dir/foo/bar.js    (GET http://example.com/foo.bar [1][2])
/root-dir/foo/bar._POST.js (POST http://example.com/foo.bar [1][2])
/root-dir/foo/bar.json.js  (GET http://example.com/foo/bar.json [3][2])
/root-dir/foo/bar.json._POST.js (POST http://example.com/foo/bar.json [3] )

Since the files are preloaded into a tree and merged with whatever is already in the tree, some handlers could be defined in the tree and some in the filesystem.

Simple URL mapping (determinate)

In the simplest case, a URL maps directly to a file's path or to the function in the tree: http://example.com/foo/bar maps to /root-directory/foo/bar.js or {foo:{bar:fn()}}.

The function is only called for GET or HEAD requests. A method-specific function must be defined for any other method.

The function will be called in context so that this is an object that is created for each request and shared by all handlers. The request object can be referenced as this.req and the response object as this.res. this.end() passes the request to the next middleware layer.

If a handler is defined for any directory (node) along the way, it will be invoked before this, the leaf node handler. Each of those handlers must call their descend callback in order for the request to reach the leaf node handler.

HTTP method (POST, PUT, etc) specific (determinate)

If only Simple URL mapping is used, only GET (or HEAD) requests are routed.

GET http://example.com/foo/bar maps to /root-directory/foo/bar.js or {foo:{bar:fn()}}.

POST http://example.com/foo/bar maps to /root-directory/foo/bar._POST.js or {foo:{'bar._POST':fn()}}.

Other methods are mapped in a similar manner.

As with a simple URL handler, the function will be called in context so that this is an object that is created for each request and shared by all handlers. If the method mapping function calls its descend callback, both the method-mapping function and the simple URL function would be called in the same context, allowing data sharing.

Handlers for URLs with extensions (determinate)

Given a URL like http://example.com/foo/bar, a HTML file might be served. Alternatively, a JSON representation of the underlying data might reasonably have a URL of http://example.com/foo/bar.json.

The function at /root-directory/foo/bar.json.js would be called to serve this request as would a function defined in the tree at {foo:{'bar.json':fn()}}

HTTP method-specific functions can also be defined, such as at /root-directory/foo/bar.json._GET.js or in the tree at {foo:{'bar.json._GET':fn()}}. These function the same as their non-extended counterparts.

So a site serving css, js and json to go with foo/bar, including special handling for POSTs to foo/bar might define the following tree:

{
    foo: {
        bar: function(descend) {
        }
        bar._POST: function(descend) {
        }
        'bar.css': function(descend) {
        }
        'bar.json': function(descend) {
        }
        'bar.js': function(descend) {
        }
    }
}

Directory requests (determinate)

We have been using a URL of /foo/bar in several examples above. But what if, for the same site, a request for /foo or for foo/ is received? Special naming conventions are used to deal with requests like these where the request is for a directory.

If the URL is http://example.com/foo (without a trailing slash), a file could be defined at /root-directory/foo.js without conflicting with the directory defined at /root-directory/foo.

But you cannot define both an object and a function at foo in the tree, so a special naming convention is employed here: appending a dot to the end of foo. The following tree would serve both foo/bar and foo:

{
  'foo.':fn(), // http://example.com/foo
  foo:{
    bar: fn()  // http://example.com/foo.bar
  }
}

Directory requests (trailing slash) (determinate)

Naming a handler for a directory request with trailing slash, such as /foo/ requires special naming conventions.

In the tree, use a slash as a key, as in the following tree: {foo:{'/':fn()}}.

_INDEX is a reserved name that designates a handler for slashed directory requests, such as a file named: /root-directory/foo/_INDEX.js

Directory default (indeterminate) handlers

It is possible to define functions that will be called for every request within a directory. So in the case of a request for http://example.com/foo/bar, a function will be called at the root level, at the foo level, and finally for the specific bar request.

The default handlers can be looked at as middleware, where the middleware stack is different for different paths.

The descend callback is more significant for a directory default handler than in most other handlers. In the other case, the handler is called at the end of the path, but here the handler is called while traversing the path. If the default handler does not call its descend callback, the request will go no further. As with other handlers, if descend is not called, then either a response needs to be sent to this.res or this.next() needs to be called.

A handler for a directory containing resources that require greater permissions, may, for example, thus block or redirect requests from those not having adequate permission.

Since this is shared by all handlers for a given request, values can be added or changed. One could, for example, create a breadcrumb object in the root directory's handler and push new values into it at each level. The common bus might also be used to maintain configuration information -- configuration that might change from one part of a site to another.

Indeterminate handlers are called without regard to HTTP method. The handler function may, of course, include code that is conditional upon the value of this.req.method.

Preloading

When a root directory, such as /root-directory is specified, FSRoute loads all the modules found in that directory and its subdirectories and organizes them into a tree. If both a tree and a root directory are specified, the two will be merged to produce a single tree.

Since require is synchronous and because preloading reasonably should occur during server initialization, the entire preloading process is synchronous.

Parallel file directories

Although the original intent of FSRoute was that all resources, such as code files, templates, CSS and client-side Javascript all be together in one directory, a conflict tends to arise between server-side Javascript files and those intended for the client. There is no easy way to separate the Javascript serving requests from code meant for the client. Since all javascript files in the directory passed to the add_modules method are preloaded, client-side Javascript should not be in that directory. Measures might also need to be taken to avoid serving raw template files.

The this.path_in(dir) function allows mapping URLs to files in any directory.

Virtual directories

One significant capability associated with indeterminate handlers is the ability to easily define virtual directories.

Our example.com website might, for example, include, in addition to everything else, a blog. But the content of the blog is dynamically generated with data from the database. In a URL like http://example.com/blog/2013/12/13 the 2013/12/13 part of the URL defines the database query.

To implement this blog, we could define a default handler for the blog directory in the tree like {blog:{'*':fn()}} or in the filesystem at /root-dir/blog/_DEFAULT.js. This would be invoked for any URL starting http://example.com/blog. Instead of calling descend, this function would perform the database lookup and produce the requested page.

A virtual directory might also be backed by a collection of static files.

this.remainder will contain the remainder of the URL path, that to the right of the known path. In this blog example, then, this.remainder would contain 2013/12/13.

Release History

(Nothing yet)

License

Copyright (c) 2013 Randy McLaughlin Licensed under the MIT license.

Written with StackEdit.

Readme

Keywords

none

Package Sidebar

Install

npm i fsroute

Weekly Downloads

0

Version

0.4.0

License

none

Last publish

Collaborators

  • randymized