Node.js, Module.Exports and Organizing Express.js Routes
Jun 26, 2012
A while ago I wrote a post explaining how require and module.exports work. Today I wanted to show a practical example of how we can use that knowledge to organize routes in an express.js project.
To recap the original post, module.exports
is how we make something within our file publicly accessible to anything outside our file. We can export anything: a class, a hash, a function or a variable. When you require
something you get whatever it exported. That means that the return value from require
can be a class, a hash, a function or a variable. The end result is that how the value returned from require
is used can vary quite a bit:
User = require('./models/user')
leto = new User()
initliaze = require('./store/inialize')
initialize(config)
Models = require('./models')
user = new Models.User()
With that in mind, I happen to be building a website that'll have two distinct sections: an API and a user-facing portal. I'd like to organize my routes/controllers as cleanly as possible. The structure that I'd like is:
/routes
/routes/api/
/routes/api/stats.coffee
/routes/api/scores.coffee
/routes/portal/
/routes/portal/users.coffee
/routes/portal/stats.coffee
This is very Rails-esque with controller namespaces and resources. Ideally, I'd like controllers under API (stats and scores) to have access to common API-based methods. How can we cleanly achieve this?
Before we can dive any further we need to understand how express loads routes. You basically attach the route to the express object. The simplest example might look something like:
express = require('express')
app = express.createServer()
app.configure ->
app.use(app.router)
app.get '/', (req, res) ->
res.send('hello world')
On line 4 we tell express to enable its routing, and then use the get
and post
methods (to name a few) to define our routes.
Taking a baby step, if we wanted to extract our route into its own file, we could do:
module.exports = (app) ->
app.get '/', (req, res) ->
res.send('hello world')
express = require('express')
app = express.createServer()
app.configure ->
app.use(app.router)
require('./routes')(app)
The syntax might be strange, but there's nothing new here. First, our exports
isn't doing anything other than exporting a function that takes a single parameter. Secondly, our require is loading that function and executing it. We could have rewritten the relevant parts more verbosely as:
routes: (app) ->
....
module.exports = routes
routes = require('./routes')
routes(app)
So how do we take this to the next level? Well, we don't want to have to import a bunch of different routes at the base level. We want to use require('./routes')(app)
and let that load all the necessary routes. So, the first thing we'll do is put an index
file in a routes folder:
module.exports = (app) ->
require('./api')(app)
require('./portal')(app)
When you tell node.js to require a folder, it automatically loads its index file. With the above, our initial route loading remains untouched: require('./routes')(app)
. Now, this loading code doesn't just include routes/index.coffe
it actually executes it (it's calling the function and passing in the app
as an argument). All our routes function does is load and execute more functions.
For both our API and portal, we can do the same thing we did for routes:
module.exports = (app) ->
require('./stats')
require('./scores')
Stats and scores can contain actual routes:
module.exports = (app) ->
app.post '/api/scores', (req, res) ->
app.get '/api/scores', (req, res) ->
module.exports = (app) ->
app.post '/api/stats', (req, res) ->
This is a good solution to help us organize our code, but what about sharing behavior? For example, many API functions will want to make sure requests are authenticate (say, using an SHA1 hash of the parameters and signature).
Our first attempt will be to add some utility methods to the api/index.coffee
file:
routes = (app) ->
require('./stats')
require('./scores')
signed = (req, res, next) ->
if is_signed(req)
next()
else
res.send('invalid signature', 400)
is_signed = (req) ->
module.exports =
routes: routes
signed: signed
Since we are now exporting a hash rather than a function, our routes/index.coffee
file also needs to change:
module.exports = (app) ->
require('./api')(app)
require('./portal')(app)
module.exports = (app) ->
require('./api').routes(app)
require('./portal').routes(app)
Now, we can use our signed method from our scores route:
api = require('./index')
module.exports = (app) ->
app.post '/api/scores', api.signed, (req, res) ->
For those who don't know, when an express route is given 3 parameters, the 2nd one is treated as something of a before-filter (you can pass in an array of them and they'll be processed in order).
This solution is pretty good, but we can take it a step further and create some inheritance. The main benefit is not having to specify api.
all over the place. So, we change our api/index.coffee
one last time:
routes = (app) ->
require('./stats')
require('./scores')
class Api
@signed = (req, res, next) =>
if @is_signed(req)
next()
else
res.send('invalid signature', 400)
@is_signed = (req) ->
module.exports =
routes: routes
Api: Api
Since the routes are still exposed the same way, we don't have to change the main routes/index.coffee
file. However, we can change our scores files like so:
class Scores extends require('./index').Api
@routes: (app) =>
app.post '/api/scores', @signed, (req, res) ->
module.exports = Scores.routes
And that, is that. There's a lot going on in all of this, but the key takeaway is that you can export anything and that flexibility can lead to some fairly well organized code. You can also leverage the fact that the index
file is automatically loaded when its folder is required to keep things nice and clean.
I know this jumped all over the place, but hopefully you'll find it helpful!