A Layered Node.js Architecture using Express
Using layers in your app is a good way to ensure separation of concerns. This post is not an in-depth description to this architecture pattern but more a quick description of each layer’s purpose and how I’ve implemented them in node using express. For the impatient, you can peruse the code of the working example here.
Software used
View Layer
- Accept some data
- Apply any required formatting
- Render
View instances wrap a template and supplies additional functions to help take the burdon off the templating system, whichever one you use. For example, text transformations, regular expressions, etc are best done outside of the template in a simple testable function. In the following AuthorView example, whilst being contrived, the format function sets the Authors name to upper case.
Controllers
- Map a URI
- Extract some parameters
- Kick off some work (it’s not concerned what nor how it gets done)
- Send the result to a View for rendering
This example illustrates the use of Controller.setupGetRoute, a convenience function which takes a route’s path, an action and a hash of Views mapped to the Accept header they respond to. The action property is a function in the current Controller instance which extracts the required parameters from the request and forwards them onto the Service Layer. Note the lack of error handling in the following AuthorController example. All errors resulting from the Service layer are passed from Controller.setupGetRoute directly to the callback, preventing clutter in the Controller instances.
For each request, the the Accept header is used to map the request to a View. If a match is made, the corresponding View’s render function is called with the request and response objects along with the result of the call to the action.
I accept that express middleware can be used instead of this call to the Service layer, however if you do you have to attach the result of the action to the request object. That feels a bit muddled to me.
Service Layer
- Do something for the Controller
- Return the result to the Controller
Try to think of the Service layer as the “do anything the Controller wants” layer. It consists of specific use-case functions, as in the following AuthorService gist. This function pull the author requested from the datastore and then grabs their books for good measure and returns the them both (or an error). The most important thing about the Service layer is that it separates the Controllers from the datastore or other sources of complexity. This ensures that writing (and testing) Controllers remains a simple task.
Services are inter-dependent singletons and are attached to the app. i.e. In the example below, the AuthorService uses the BookService to get the Author’s Books.
Data Access Layer
- Put, Get, Update and Delete datastore entries.
The Data Access Layer is our last layer. It is hard-tied to the datastore of your choice and manages every interaction with it. Once again, encapsulation is the key. Having all of your database code decoupled from your Controllers and Services makes them easier to test. It also makes moving from one datastore to another a much less painful process.
In the example code, each dao is tied to its own collection. In so, calling getList from AuthorDao will return a list of authors, from BookDao, books and so on. For now the example only handles Put and Get operations to keep the example from growing too large. Feel free to send a pull request!
Working Example
Pull a clone of the working example here: https://github.com/dave-elkan/layered-express
Notes on Models
Mongoose is making great strides to becoming the ORM for node. It does a wonderful job of taking care of the heavy lifting of MongoDB abstraction, validation, defaults, etc. However I don’t use it in this example as one of my main goals with using a layering technique is that sources of Error are centralised and handled in as few places as possible. Mongoose uses an “enriched model” pattern (where every model object has a save, update and delete function). Whilst this is very convenient, I wanted to enforce my database transactions to take place from one central point.