So you’ve decided to join thousands of other companies and use Express.js to build a REST API. How should you structure your project?
One of the great things about Express is that it’s a very light and unopinionated framework which makes it extremely flexible in how you set it up.
A possible downside is that there isn’t much guidance on how to structure applications. This can lead to issues around maintenance and scaling.
In this post, we’ll consider some best practice approaches to structure a robust and high-performing Express REST API project.
Why is structure important?
Why does structure matter? Why not use a “move fast and break things” approach and put your code wherever it seems to work?
Without good structure, it will become increasingly hard to understand your code and easier to accidentally break things. This means bugs increase and deployments become slower and riskier.
If you plan to scale your API the decisions you bake in earlier should support that. Otherwise, you’ll need to spend considerable time and expense rewriting your application.
While there are a variety of recommendations below, they are all based on the same principles:
- Separation of concerns. Organizing functions and modules to ensure they have a single, clear task which ensures your code is easy to read and maintain.
- Modular architecture. Composing your app in pieces that are isolated and easy to understand. This ensures your code is flexible and allows it to be recomposed for cron tasks, unit testing, etc.
Keeping these principles in mind means you’ll know what to do when your requirement go beyond those presented here and in other examples.
Separate app and server
The first thing you’ll typically do in an Express API project is to configure a basic app and assign it to an HTTP server. This is commonly, but incorrectly, done in a single file:
Since there are two entities here, the app and the server, they should be separated. Not just on principle, but also because separating the app and server also allows you to unit test your app without initializing the server.
So let’s refactor this. We’ll have an
app folder where we’ll keep the application code and create an
index.js module inside where we configure the app.
We’ll put the
server.js file in the project root. This file will import the app and assign an HTTP server. It will also serve as the entry point of our app.
We could also add additional network-related configuration in this file vhosts, SSL, etc.
Three-layer app architecture
To structure our API app we’ll use the popular “three-layer architecture”.
1. Web layer. Responsible for sending, receiving, and validating HTTP requests. Common configuration here includes routes, controllers, and middleware.
2. Service layer. Contains business logic.
3. Data access layer. Where we read and write to a database. We typically use an ORM like Mongoose or Sequelize.
This architecture creates a good separation of concerns that will make it easy to read and maintain the code.
It’s also modular enough to allow for recomposition. For example, you may want to create interfaces for cron tasks or CLI commands. These can seamlessly replace the web layer.
Let’s now look in more detail at these layers and their contents.
The web layer is where we process HTTP requests, dispatch data to the service layer, then return an HTTP response.
Anything dealing with the req and res objects lives here. The main abstractions we utilize in this layer are:
Routes. Where you declare the path of API endpoints and assign to controllers.
Middleware. Reusable plugins to modify requests typically used for cache control, authentication, error handling, etc.
Controllers. The methods that process an endpoint and unpack web layer data to dispatch to services.
Here’s an example file structure with product and user example entities.
app index.js routes products.js users.js controllers products.js users.js middleware cacheNoStore.js server.js
In simple projects you will often see the routes and logic declared together:
A best practice is to abstract routes into a module that has the job of mapping paths to controller methods.
It’s also a good idea to have a separate routes file for each API entity e.g. products, users, etc.
Here’s an example routes file:
If you have reusable functionality like cache control, access checks, or input validation, it’s a good idea to abstract these into middleware plugins.
These can then be applied on a per-route basis in the routes file:
With your route modules created, you can declare them in your app index file. Note that a path prefix can also be set here.
The best practice for controllers is to keep them free of business logic. Their single job is to get any relevant data from the req object and dispatch it to services. The returned value should be ready to be sent in a response using res.
Make sure you don’t pass any web layer objects (i.e. req, res, headers, etc) into your services. Instead, unwrap any values like URL parameters, header values, body data, etc before dispatching to the service layer.
Here’s an example controller module.
Data access layer
The final layer to consider in our app structure is the data access layer. This is the layer that communicates with your database.
Most modern applications will use an ORM like Mongoose or Sequelize. The models you create for these will serve as your data access layer.
app index.js routes products.js users.js controllers products.js users.js middleware cacheNoStore.js models product.js user.js services products.js users.js server.js
Separation into components
In some scenarios, you may consider a further degree of separation: components.
As explained on the Node Best Practices site, if you have logically independent API features (e.g. an admin API and a user API) it’s a good sign they should be separated into components.
To implement a component structure, the app folder we considered above would be refactored and duplicated to be named based on each entity.
Note that we’ve provided common folders for middleware and models as these will be likely shared utilities.
products index.js routes.js controllers.js services.js users index.js routes.js controllers.js services.js middleware cacheNoStore.js models product.js user.js server.js
Treblle is API observability software that can be installed in an Express app with just a few lines of code. It will monitor and trace any issues with your API so that they can be caught and fixed before affecting your uses.
In fact, the Treblle JS SDK also supports Node, Express, KoaJS, Strapi, and Cloudflare Workers!
In addition to API observation, Treblle offers your organization a variety of other useful features including:
Auto-generated API documentation
API quality scores
1 click testing
And more. Treblle is free for up 30,000 requests and can be installed in minutes, so give it a try!
Express is one of the most popular Node.js frameworks, appreciated for its speed and flexibility. If you aren’t careful, though, it’s easy to create an untamed codebase that becomes a hassle to maintain and scale.
By practicing the principles of separation of concern and of modular architecture, we can ensure our Express APIs are robust and flexible.