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:
# the user file exported a class User = require('./models/user') leto = new User() # the initialize file exported a function initliaze = require('./store/inialize') initialize(config) # the models file exported as hash (which has a User key which was an object) 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
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:
# routes.coffee module.exports = (app) -> app.get '/', (req, res) -> res.send('hello world') # and use it like so: 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.coffee routes: (app) -> .... module.exports = routes # and use it like so: 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:
# routes/index.coffee 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:
# routes/api/index.coffee module.exports = (app) -> require('./stats') require('./scores')
Stats and scores can contain actual routes:
# routes/api/scores.coffee module.exports = (app) -> app.post '/api/scores', (req, res) -> # save as score app.get '/api/scores', (req, res) -> # get scores # routes/api/stats.coffee module.exports = (app) -> app.post '/api/stats', (req, res) -> # save a stat
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
# routes/api/index.coffee routes = (app) -> require('./stats') require('./scores') signed = (req, res, next) -> if is_signed(req) next() else res.send('invalid signature', 400) is_signed = (req) -> #how to sign a request isn't the point of this port 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:
# OLD routes/index.coffee module.exports = (app) -> require('./api')(app) require('./portal')(app) # NEW routes/index.coffee module.exports = (app) -> require('./api').routes(app) require('./portal').routes(app) #assuming we do the same to portal
Now, we can use our signed method from our scores route:
# routes/api/scores.coffee api = require('./index') module.exports = (app) -> app.post '/api/scores', api.signed, (req, res) -> # save as score
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/api/index.coffee 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) -> #how to sign a request isn't the point of this port 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) -> # save as score 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!