Web development/model-router

From ISoft Wiki
Jump to navigationJump to search

model-router does a few things that make writing data servers easier:

  • Routing
  • Session handling/state
  • Wrapping up authentication
  • Checking for parameters existence

Including

You could have multiple router-things if you wanted to - you know, in case you were running multiple sites from the same node.js server, or something - but for your purposes you'll probably just want to instantiate one.

var Router = require('model-router')
var router = new Router()

It doesn't launch any servers of its own, that would be rude. You'll want to create your own server in the usual way, and then tell model-router to handle it if appropriate.

model-router only sends back JSON responses, and doesn't do anything with static content out of the box, so if you want to serve HTML/JavaScript files or something, you'll have to do it yourself:

require('http').createServer(function(req, res) {
	var pathname = require('url').parse(req.url).pathname
	var match
	if (match = /^\/static(\/.*)$/.exec(pathname)) {
		require('send')(req, match[1]).root('./static/').pipe(res)
	} else {
		router.handleRequest(req, res)
	}
}).listen(8080)

Routing

All right, your server is running. You want to add a new response to certain messages - so that requests to yourdomain.com/information/time would get the current server time back, or something dumb like that.

router.route('/information/time', function(parameters, session, next) {
	next(new Date())
})

Browsing to the URL should immediately get you back this response:

{"success":true,"output":"2013-02-19T22:00:56.507Z"}

But let's say that something bad happens on the server - maybe it's an error if a client asks for the server time on a Sunday. You could reflect that by rewriting the function like so:

router.route('/information/time', function(parameters, session, next) {
	var date = new Date()
	if (date.getDay() === 0) {
		var err = new Error("Don't bother me on Sunday!")
		err.code = 'RESTING'
		next(err)
	} else {
		next(date)
	}
})

Visiting that same URL on a Sunday would get you this response instead:

{"success":false,"message":"Don't bother me on Sunday!","code":"RESTING"}

In realistic scenarios, you'll probably want to only respond with errors in the case of legitimate server errors, like a query failing or a missing file or something.

Session state

All right, so you can easily fling data back to the client. Unbeknownst to you (before you read this sentence), it turns out that model-router also stores a session variable for each visitor (using a cookie that it sets on the client)! You can set parameters on this object whenever you like, and access them again later:

router.route('/counter/increment', function(parameters, session, next) {
	session.counter = (session.counter || 0) + 1
	next()
})

But now let's say that you want to see the current value of that pointless counter! But let's use a new feature that I feel like telling you about: guaranteed parameters. Like, you know, where a request is only valid if a certain value is provided? You could do a bunch of input parsing, and check to see if certain values were defined or whatnot, but model-router handles some of that for you.

Let's only give back the current value if the user provides a parameter called "please" - we won't even bother to check what it is, as long as it's set:

router.route('/counter/value', function(parameters, session, next) {
	next(session.counter || 0)
}, ['please'])

If you don't pass in a parameter with the request (either as part of the query string in the URL, or in the headers), you'll get back this response:

{"success":false,"message":"You must provide these parameters: please","code":"MISSING_PARAMETERS"}

...but if the client does visit /counter/value?please=lol or whatever, they'll totally get the value back! And remember, that value is session-specific - visiting the same URL in a different browser could get you a completely different number.

It's up to you to maintain whatever state you like in those session variables. Obvious candidates are user information, and state about current high-traffic jobs that the user is engaged in (if they're sending requests every second, you probably don't want to hit the database more than is necessary).

You can (and probably want to) have session state expire after a certain amount of time. This function call will cause sessions to expire after 10 minutes of inactivity:

router.setSessionInactivityTimeout(10, function(session) {
	console.log("A user just timed out!  They got their counter up to " + session.counter + " first, though.")
})

Authentication

But you probably want to have users log in before you do anything else, right? Well, hold on to your pants, because model-router makes that easy too.

router.route('PUT', '/authenticate', function(parameters, session, next) {
	if (parameters.username === 'duff' && parameters.password === 'butts123') {
		session.username = 'duff'
		next("You're logged in now!")
	} else {
		next(new Error('Invalid login!'))
	}
}, ['username', 'password'])

router.setSessionAuthentication(function(parameters, session, next) {
	var logged_in = typeof session.username !== 'undefined'
	next(logged_in)
})

Wait, what's that extra parameter there at the beginning? Oh, that's just how you specify a route for a specific HTTP method! The above code will only route requests to /authenticate to that function if the HTTP request was the PUT method. If you want, you can still specify a handler for /authenticate without giving a method - in that case, the most specific routing will be used.

We can now define routes that are only accessible to authenticated users:

router.routeAuthenticated('/counter/vip-value', function(parameters, session, next) {
	next((session.counter || 0) * 10)
})

Try to visit /counter/vip-value without having authenticated, and you'll see

{"success":false,"message":"You have to be authenticated to access /counter/vip-value","code":"NOT_AUTHENTICATED"}

...but visit it after authenticating, and you'll see that your session's magical incrementable value is much larger than it would otherwise appear! Zoinks!