Route Separation Example
This example demonstrates a pattern for organizing Express route definitions into separate modules, each responsible for a distinct resource or functional area. By moving route handlers out of the main application file and into dedicated modules, the codebase becomes more maintainable, testable, and scalable.
Overview
The route separation pattern addresses a common challenge in Express applications: as the number of routes grows, a single index.js or app.js file becomes difficult to navigate and reason about. This example splits route handling across three modules:
- site – handles the root/index page
- user – handles user CRUD operations
- post – handles post listing
Each module exports one or more handler functions. The main application file (index.js) imports these modules and wires them to specific HTTP methods and URL paths.
How Routes Are Separated
The separation works in two layers:
-
Handler definitions live in separate files. Each resource module (e.g.,
user.js,post.js) contains the logic for handling requests to that resource. These modules may also contain their own data stores (in this example, faux in-memory databases). -
Route definitions remain in the main file. The main
index.jsis responsible for mapping URL patterns to the handler functions exported by the resource modules. This keeps the routing table visible in one place while the implementation details are encapsulated elsewhere.
Loading Route Modules
The main application file loads each resource module using require():
var site = require('./site');
var post = require('./post');
var user = require('./user');Each module exports an object with handler functions. For example, post.js exports a single list function:
exports.list = function(req, res){
res.render('posts', { title: 'Posts', posts: posts });
};Defining Routes
Routes are defined using Express's standard HTTP method functions (app.get(), app.put(), app.all()), but the second argument is a reference to the exported handler rather than an inline function:
// General
app.get('/', site.index);
// User
app.get('/users', user.list);
app.all('/user/:id/:op?', user.load);
app.get('/user/:id', user.view);
app.get('/user/:id/view', user.view);
app.get('/user/:id/edit', user.edit);
app.put('/user/:id/edit', user.update);
// Posts
app.get('/posts', post.list);This pattern keeps the routing logic declarative and readable. The handler functions themselves are defined in their respective modules, where they can access module-scoped data and helper functions.
Benefits of Route Separation
- Modularity – Each resource has its own file, making it easy to locate and modify related code.
- Testability – Handler functions can be imported and tested in isolation without spinning up the full Express application.
- Reusability – Modules can be reused across different applications or mounted at different URL prefixes.
- Collaboration – Multiple developers can work on different resource modules simultaneously without merge conflicts.
- Scalability – New resources can be added by creating a new module and adding a few lines to the main routing table.
URL Prefix Handling
This example does not use Express's built-in app.use() with a path prefix to mount entire route modules. Instead, each route is defined individually with its full path. For example, all user routes start with /user or /users, and post routes start with /posts.
To add a prefix to all routes in a module, you would either:
- Add the prefix to each route definition (as done here), or
- Use
express.Router()with a mount path (e.g.,app.use('/user', userRouter)), which is a more advanced pattern not shown in this example.
The app.all('/user/:id/:op?', user.load) line demonstrates a catch-all route that matches any HTTP method for user paths with an optional operation segment. This allows the load middleware to run before the specific handler for each user route.
Module Reference
site Module
File: examples/route-separation/site.js
Purpose: Handles the root/index page of the application.
Public API:
| Symbol | Signature | Purpose |
|---|---|---|
index | function(req, res) | Renders the site index view |
user Module
File: examples/route-separation/user.js
Purpose: Handles all user-related operations including listing, viewing, editing, and updating users.
Public API:
| Symbol | Signature | Purpose |
|---|---|---|
list | function(req, res) | Renders a list of all users |
load | function(req, res, next) | Middleware that loads a user by ID from the faux database and attaches it to req.user |
view | function(req, res) | Renders a single user's details |
edit | function(req, res) | Renders the user edit form |
update | function(req, res) | Processes the user update form submission |
post Module
File: examples/route-separation/post.js
Purpose: Handles post-related operations (listing only in this example).
Public API:
| Symbol | Signature | Purpose |
|---|---|---|
list | function(req, res) | Renders a list of all posts |
Comparison and Relationships
The three modules follow the same pattern but differ in complexity:
| Aspect | site | user | post |
|---|---|---|---|
| Number of handlers | 1 | 5 | 1 |
| Uses middleware pattern | No | Yes (load as middleware) | No |
| Data store | None (static view) | Faux database | Faux database |
| HTTP methods used | GET | GET, PUT, ALL | GET |
The user module is the most feature-rich, demonstrating how a single resource can have multiple handlers that work together. The load handler acts as middleware, populating req.user before the view, edit, or update handlers run. This is a common Express pattern for reducing duplication.
The site and post modules are simpler, each exposing a single handler. They show that not every module needs to be complex; the pattern scales down as well as up.
All three modules are independent of each other. They share no state and have no dependencies between them. The only coupling is through the main index.js file, which imports them all and defines the routing table.