Bootstruct
Routing by structure.
A name-convention framework for Node.js
Get started
-
Install Bootstruct in a new folder:
$ npm install bootstruct
-
Create an index.js
file and an app
folder.
-
Copy the following to your index.js
file:
var http = require('http');
var bts = require('bootstruct');
var app = bts('app');
http.createServer(app).listen(1001, function(){
console.log('Listening on port 1001');
});
-
Inside app
folder, create a get.js
file with the following content:
module.exports = function (io) {
io.res.end('hello beautiful world');
};
-
Start your server up:
$ node index.js
You're now ready for GET requests to yourdomain.com:1001/
NOTE: You can use post
, put
and delete
(.js) as well. They are all reserved names for files and folders in Bootstruct.
Table of contents
intro
"Coding by convention" or "configuration over code" or whatever. They are all fine and it's all a matter of personal taste and project's needs.
Bootstruct does it with names convention. "Routing by structure" if you'd like.
As such, learning Bootstruct is more about understanding how it reads your folder-structure and behave based on this structure than code and syntax.
Overview
What Bootstruct does?
* Bootstruct saves you from coding your routes
* Bootstruct enforces a good code seperation by design
* Bootstruct gives you intuitive control over requests' flow
* Bootstruct provides you with nice RESTfull URLs
To support routes like:
domain.com/
domain.com/foo
domain.com/foo/bar
you don't have to write any code but the handler functions themselves. Just structure your app folder like the following and export your handlers from the index.js
files like in the Get started example above:
├── app
│ ├── index.js <── called for all requests to: '/'
│ └── foo
│ ├── index.js <── called for all requests to: '/foo'
│ └── bar
│ └── index.js <── called for all requests to: '/foo/bar'
If you're familiar with express/connect, the equivalent would be:
app.all('/', function () {
});
app.all('/foo', function () {
});
app.all('/foo/bar', function () {
});
NOTE: You can use all
instead of index
if you'd like.
get, post, put, delete
You probably want to be more specific about different types of request methods. Just add verb-files where you need them (e.g. get.js
, post.js
):
├── app
│ ├── index.js <── called for ALL requests to: '/'
│ ├── get.js <── called for GET requests to: '/'
│ └── foo
│ ├── index.js <── called for ALL requests to: '/foo'
│ ├── get.js <── called for GET requests to: '/foo'
│ ├── post.js <── called for POST requests to: '/foo'
│ └── bar
│ └── index.js <── called for ALL requests to: '/foo/bar'
You don't have to use an index.js
:
├── app
│ ├── get.js <── called for GET requests to: '/'
│ └── foo
│ ├── get.js <── called for GET requests to: '/foo'
│ ├── post.js <── called for POST requests to: '/foo'
│ └── bar
│ └── get.js <── called for GET requests to: '/foo/bar'
If an index.js
exists it will always run before the verb and they are both called only for the target folder. The target-folder is the last folder whose name found in the request pathname (e.g. bar
on request to: /foo/bar
).
express/connect equivalent would be:
app.all('/', function () {
});
app.get('/', function () {
});
app.all('/foo', function () {
});
app.get('/foo', function () {
});
app.post('/foo', function () {
});
app.all('/foo/bar', function () {
});
A "stale" folder
If bar
, for example, is a "stale" folder (has no sub-folders and no other methods but index
), you can cut the overhead of a folder and turn it into a file:
Before:
├── app
│ ├── index.js
│ └── foo
│ ├── index.js
│ └── bar <── folder
│ └── index.js
After:
├── app
│ ├── index.js
│ └── foo
│ ├── index.js
│ └── bar.js <── file
Scale It Up
If any method file gets bigger, turn it into a folder:
Before:
├── app
│ ├── index.js
│ ├── get.js
│ ├── post.js <── file
│ ├── put.js
│ └── delete.js
After:
├── app
│ ├── index.js
│ ├── get.js
│ ├── post <── folder
│ │ ├── index.js
│ │ ├── dependency_1.js
│ │ └── dependency_2.js
│ ├── put.js
│ └── delete.js
'verbs' folder
If you use all verbs, also having multiple sub-folders can hurt your eyes:
├── app
│ ├── about
│ ├── contact
│ ├── delete.js
│ ├── get.js
│ ├── index.js
│ ├── messages
│ ├── post.js
│ ├── profile
│ └── put.js
For the sake of your eyes, you can use a verbs
folder, just as a namespace to contain the verbs entries:
├── app
│ ├── about
│ ├── contact
│ ├── index.js
│ ├── messages
│ ├── profile
│ └── verbs <──
│ ├── get.js
│ ├── post.js
│ ├── put.js
│ └── delete.js
NOTE: index
and all
both can exist inside a verbs
folder.
In case of duplicates, the entry outside verbs
will take place:
├── app
│ ├── all.js <── this will run
│ └── verbs
│ ├── all.js <── this won't
│ ├── get.js
│ └── post.js
noVerb
Consider:
├── app
│ ├── all.js
│ └── get.js
Now let's say you get a POST
request. Naturally, Bootstruct will skip the verb and will only run the all
method. If you want to handle unsupported verbs requests individually you can use a noVerb
entry. Controllers run their noVerb
method on unsupported verbs requests, but only if they have at least one verb.
├── app
│ ├── all.js
│ ├── get.js
│ └── noVerb.js <── gets called for any type of requests other than `GET`
├── app
│ ├── all.js
│ ├── *
│ └── noVerb.js <── doesn't get called because no verbs at all
This saves you some of the logic you would normally put in you all
method, regarding request.method
.
NOTE: You can use a noVerb
entry inside a verbs
folder as well.
TIP: 405
is the server status code for "Method not allowed".
noVerb
is a special method in Bootstruct because when exists, it's being delegated from the parent folder to all of its sub-folders recursively.
├── app
│ ├── get.js
│ ├── noVerb.js <── gets called for unsupported requests to both: '/' and '/foo'
│ └── foo
│ ├── get.js
│ └── bar.js
first & last
As we saw earlier, the index
and the verb entries (files or folders) only run for the target-folder.
If you want a folder to do something even if it's not the target-folder but its name was addressed in the request (e.g. foo
in foo/bar
), you could use 2 other methods: first
and last
. Both are called when a folder is requested whether or not it's the target-folder or should the request be passed on. first
is called before the target-folder is done with its index
and the verb method. last
is called after the target-folder is done.
├── app
│ ├── index.js
│ ├── get.js
│ └── foo
│ ├── first.js <──
│ ├── index.js
│ ├── get.js
│ ├── post.js
│ ├── bar
│ │ ├── first.js <──
│ │ ├── index.js
│ │ └── last.js <──
│ └── last.js <──
We'll see how these first
and last
methods fit in the flow in a sec.
Reserved Entry Names
All of these names are all reserved names for entries (files or folders) in Bootstruct. You name your entries with these names and get a certain behavior.
1. first - first thing to run in a folder
2. verbs - just a namespace folder to hold your verb handlers
3. index - called on all HTTP requests ─┐
4. all - called on all HTTP requests │
5. get - called on `GET` HTTP requests │
6. post - called on `POST` HTTP requests ├─ on target folder only
7. put - called on `PUT` HTTP requests │
8. delete - called on `DELETE` HTTP requests │
9. noVerb - called on unsupported verbs requests ─┘
10. last - last thing to run in a folder
Custom named entries (like foo
or bar
) become controllers which are URL namespace handlers for requests containing their name (e.g. /foo
and /foo/bar
).
Reserved entry names are parsed as those controllers' different methods and they are called when needed according to their role listed above. Methods expected to export a single function that accept io
as a single argument (see Get started for an example) and they pass this io
from one to another.
An example of a pseudo object that describes foo
controller with a bar
sub-controller:
var foo = {
first: require('foo/first'),
index: require('foo/index'),
verbs: {
get: require('foo/get'),
post: require('foo/post'),
},
subControllers: {
bar: {
first: require('foo/bar/first'),
index: require('foo/bar/index'),
verbs: {
get: require('foo/bar/get'),
post: require('foo/bar/post'),
},
subControllers: null,
last: require('foo/bar/last')
}
},
last: require('foo/last')
};
Something very similar is generated on Bootstruct init, when your app
folder is being parsed.
io
express/connect middleware functions accept 2-3 arguments: request
, response
and next
.
Bootstruct methods handles only a single argument: io
.
io
is an object that holds the native request/response as properties and a next
method (and more):
io.req
io.res
Both by reference, untouched.
io.next()
is for you to call from within your methods when they are done and the io
is ready for the next method.
With express/connect:
app.get('/foo', function(req, res, next){
res.send('hello world');
next();
});
With Bootstruct:
module.exports = function (io) {
io.res.send('hello world');
io.next();
};
io.params
Bootstruct refers the different URL parts as parameters so it splits the URL by slashes (pathname only) and stores the returned array in io.params
.
On request to: /foo/bar/aaa/bbb
io.params
starts as: ['foo', 'bar', 'aaa', 'bbb']
.
Starting at your app
folder, Bootstruct uses io.params
to check if app
folder is the target-folder by checking the first item for an existing sub-folder. If the first item (e.g. foo
) is a sub-folder, app
is not the target. Next, foo
removes its name from io.params
(always the first item) and checks the new first item (e.g. bar
) for a sub-folder and so on. This way the target-folder (bar
) is left with the params that are not controllers in your app (e.g. ['aaa', 'bbb']
).
A Request to: /foo/bar/john
:
with express/connect:
app.get('/foo/bar/:name', function(req, res, next){
console.log(req.params.name);
next();
});
and with Bootstruct:
module.exports = function (io) {
console.log(io.params[0]);
io.next();
};
io other props
- io._ctrl - (internal) The current handling controller
- io._profiles - (internal) io's state in all controllers
Bootstruct Flow
Consider a structure:
├── app
│ ├── first.js
│ ├── index.js
│ ├── get.js
│ ├── last.js
│ └── foo
│ ├── first.js
│ ├── index.js
│ ├── get.js
│ ├── last.js
│ └── bar
│ ├── first.js
│ ├── index.js
│ ├── get.js
│ └── last.js
There are 3 levels of nested folders as before: app/foo/bar
.
Each has the following methods:
first
index
- a verb (
get
) last
NOTE: This is a full use case. You don't have to use all of the possible methods for every folder.
Consider all of these files contain:
module.exports = function (io) {
console.log(__filename);
io.next();
};
i.e. logs the current file path and moves on to the next method. This will make Bootstruct's flow "visible" to you.
The following are examples of different requests supported by the given structure (GET requests only) and their expected logs.
NOTE: To make it more easy on the eye, preceding long/path/to/app
and .js
extensions were removed from the log.
url: /
logs:
app/first
app/index
app/get
app/last
url: /foo
logs:
app/first
app/foo/first
app/foo/index
app/foo/get
app/foo/last
app/last
url: /foo/bar
logs:
app/first
app/foo/first
app/foo/bar/first
app/foo/bar/index
app/foo/bar/get
app/foo/bar/last
app/foo/last
app/last
NOTE: Do you see the onion-like layers? me too!
The Shorter Version
This is what happens for every request. Mind the loop:
- Check-in: A folder run its
first
method. - Check: IsTarget?: Bootstruct checks the next URL part. Is there a matching sub-folder in the current one?
If so, the folder passes the io
to that sub-folder for a check-in. Back to 1.
If not, current folder is the target-folder. It will run its index
method and its verb
methods. - Check-out: The folder run its
last
method. - the folder passes the
io
back to its parent folder for a check-out. Back to 3.
Important notes:
- Bootstruct is CaSe-InSeNsItIvE when it comes to URLs and file names.
- Bootstruct ignores trailing slashes in URLs and merges repeating slashes.
- Bootstruct ignores entries that their names start with an underscore and doesn't parse them (e.g.
_ignored.js
). - You can use the
io
to hold different properties through its cycle. - The context of the
this
keyword inside method functions refers to the current controller object.
Each controller has a name (like bar
) and a unique ID which is its folder path (e.g. app/foo/bar
).
Try to log this.name
and this.id
in your different methods. - Bootstruct haven't been tested for production environment. Yet.
More to come.
Questions, suggestions, bugs, hugs, criticism or kudos are all welcome.
taitulism(at)gmail(dot)com