Zac Fukuda
065

Express.js File/Folder Structure
MVC & Colocation

Although there are new frameworks like Next.js and NestJS to meet emerging technical needs, Express is still by far the most popular framework of Node.js.

The former two frameworks are opinionated, which means how to structure your project is instructed by the frameworks. On the other hand, Express is un-opinionated. You can structure your project as you like. This free-to-structure gives us a margin to customize the application best fit into our own project, to adjust the future changes.

Nevertheless, this flexibility comes at some expense: a mess. Each developers in the team can add a new file to any folder, can give a file any name.

You have free will. As a free agent you like Express over other frameworks. It is good to use un-opinionated frameworks. I like them, too. But if you use one un-opinionated framework with discipline, it can be better.

In this article I would like to show you the two popular structuring pattern of Express: mvc and Colocation. After introducing the structure, I will list up a few concerns that have to do with developing Express—or any Node.js—application.

MVC

You don’t need an explanation for Model-View-Controller(mvc). This pattern adapted by Ruby on Rails and Laravel. Since early developers of Node.js application were either experienced in these frameworks or imitated them, the mvc is the first choice of structure for many developers when getting started with Express.

In mvc Express applications are usually structured as follows.

.
├── 📁 configs
│   ├── app.config.js
│   └── db.config.js
├── 📁 controllers
│   ├── book.controller.js
│   └── user.controller.js
├── index.js
├── 📁 middlewares
│   ├── has-token.js
│   └── is-admin.js
├── 📁 models
│   ├── book.model.js
│   └── user.model.js
├── 📁 public
├── 📁 routers
│   ├── book.router.js
│   ├── router.js
│   └── user.router.js
├── 📁 services
│   ├── book.service.js
│   └── user.service.js
├── 📁 test
│   ├── 📁 integration
│   └── 📁 unit
│       ├── 📁 services
│       │   ├── book.service.test.js
│       │   └── user.service.test.js
│       └── 📁 utils
│           └── foo.test.js
├── 📁 utils
│   └── foo.js
└── 📁 views
  • configs: configuration files for the application. This folder should never be committed.
  • controllers: files that handles the incoming requests, sends responses.
  • middlewares: business logics to be handled before request is passed to the controllers.
  • models: object–relational mapping(orm) files.
  • public: static files like CSS, JavaScript, images. I recommend to store static files to content delivery network(cdn) as much as possible. Only files like favicons, sitemaps, manifests, robots.txt, humans.txt shall be stored in this directory.
  • routers: files that define how an application responds to a client request to a particular endpoint and http method. For why I prefer routers to routes, see Routes v.s. Routers.
  • services: business logics more specific to one feature.
  • test: test files. Major test frameworks are Jest, Mocha, and Jasmine.
  • utils: miscellaneous files that cannot be categorized into the other folders but necessary for application.
  • views: Pug, Mustache, or EJS files. See Using template engines with Express.

Sourcing

Some people would like to put application codes into src folder, especially when you write codes in TypeScript then compile to JavaScript at or before release for production. In which case, the file structure would be like below:

.
├── 📁 configs
│   ├── app.config.js
│   └── db.config.js
├── 📁 public
├── 📁 src
│   ├── 📁 controllers
│   │   ├── admin.book.controller.js
│   │   ├── admin.controller.js
│   │   ├── admin.user.controller.js
│   │   ├── api.books.comments.controller.js
│   │   ├── api.books.controller.js
│   │   ├── api.controller.js
│   │   ├── api.users.controller.js
│   │   ├── book.comment.controller.js
│   │   ├── book.controller.js
│   │   └── user.controller.js
│   ├── index.js
│   ├── 📁 middlewares
│   │   ├── has-token.js
│   │   └── is-admin.js
│   ├── 📁 models
│   │   ├── book.model.js
│   │   └── user.model.js
│   ├── 📁 routers
│   │   ├── admin.book.router.js
│   │   ├── admin.router.js
│   │   ├── admin.user.router.js
│   │   ├── api.books.comments.js
│   │   ├── api.books.router.js
│   │   ├── api.router.js
│   │   ├── api.users.router.js
│   │   ├── book.comment.router.js
│   │   ├── book.router.js
│   │   ├── router.js
│   │   └── user.router.js
│   ├── 📁 services
│   │   ├── book.service.js
│   │   └── user.service.js
│   └── 📁 utils
│       └── foo.js
├── 📁 test
│   ├── 📁 integration
│   └── 📁 unit
│       ├── 📁 services
│       │   ├── book.service.test.js
│       │   └── user.service.test.js
│       └── 📁 utils
│           └── foo.test.js
└── 📁 views

Since the files in the configs, public, test and views will not be compiled, those folder shouldn’t be in the src.

You might compile src into dist or build folder. It is the best practice that you can run the application from src or dist/build folder. At local computer you would run the application from the src with the help of nodemon and ts-node. Before committing a major change, you compile TypeScript to JavaScript, run the app from dist/build in the more-production-like environment as a final check.

Colocation

Colocation is a file structuring strategy in which relevant files are placed closely together as much as possible.

For instance, in mvc files responsible for book are sorted into three or more folders.

# MVC
/controllers/book.controllers.js
/models/book.model.js
/routers/book.router.js
/views/book/book.list.pug

In colocation, the files would be saved in the following structure:

# Colocation
/book/book.controller.js
/book/book.model.js
/book/book.router.js
/book/views/book.list.pug

So, your whole Express application files would be stored in the following way:

.
├── 📁 api
│   ├── api.middleware.js
│   └── api.router.js
├── 📁 book
│   ├── book.handlers.js
│   ├── book.js
│   ├── book.middleware.js
│   ├── book.router.js
│   └── index.js
├── 📁 configs
│   ├── app.config.js
│   └── db.config.js
├── index.js
├── 📁 middlewares
│   ├── has-token.js
│   └── is-admin.js
├── 📁 public
├── router.js
├── 📁 test
│		├── 📁 integration
│		└── 📁 unit
│				└── utils
│						└── foo.test.js
├── 📁 user
│   ├── index.js
│   ├── user.handlers.js
│   ├── user.helpers.js
│   ├── user.js
│   ├── user.middleware.js
│   └── user.router.js
└── 📁 utils
    └── foo.js

Just like in mvc, you can put TypeScript codes into src folder.

In the structure above, I intentionally avoid the term controller and use handler instead. This is not conventional but Express official documentation uses handler. See Basic routing.

Why Colocation

I believe there are two main advantages you can take from colocation over mvc.

The first reason is to open a file. When you develop user feature in mvc, you find yourself opening many folders: controllers, models, routers. You might have had scrolled down and up the folder tree displayed on the left side of your text editor, over and over again. This is very stressful. If all controller, router, model files are located next to each other, colocated, you don’t have to scroll the left pane. All files you want to edit are just there.

The other reason is duplication. Imagine you have two almost identical features. The only difference the two have is a keyword. Let it be news and blog. After you have done developing news, the only thing you have to do to make blog feature is to copy news folder and update the keyword. You don’t have to search all news files scattered across the project folder unlike in case of mvc. Probably the whole duplication process might take a few minutes.

Besides the major reasons above, there is a time when you have shut down one feature. To shut down one feature, all thing you have to do is to delete the target folder. There will be less chance that you forget to delete one file. Also, all relevant files put in the same folder makes back-up easier.

The Problems of Colocation

Colocation is great. But it is not perfect. There is always a trade-off.

One of problems when you organize codebase in colocation way is views. If you make a production codebase without any compilation this shouldn’t be a problem. But if you compile, from TypeScript to JavaScript, where to place view files causes a headache. Following the colocation philosophy, you might organize your file like this:

/src/user/user.ts
/src/user/views/user.profile.pug

Pug files is not a TypeScript file. To copy Pug files from src to dist/build folder, you will need extra careful build process.

The other problem is when you implement api and administration. You can think of two ways to organize those features:

# Option 1
/api/user/api.users.handlers.js
/api/user/api.users.router.js
/admin/user/admin.user.handlers.js
/admin/user/admin.user.router.js
/user/user.handlers.js
/user/user.router.js

# Option 2
/user/user.handlers.js
/user/user.router.js
/user/user.api.handlers.js
/user/user.api.router.js
/user/user.admin.handlers.js
/user/user.admin.router.js

There is no right answer for which option is better. If you respect uris, i.e. /api/users, /admin/user, /user, you might think the option 1 is better off. This, however, violates the concept of colocation “place relevant files closely together.” So you might feel option 2 is more natural in colocation although there could be slight vexation when you define routers. For example:

/api/router.js
import { Router } from 'express';
import userApiRouter from '../user/user.api.router';

const apiRouter = Router();
apiRouter.use('/users', userApiRouter);

export default apiRouter;

Things to be Considered

Besides whether your project is structured in mvc or colocation way, there are more things to be considered to develop cleaner more maintainable application.

Entry Point

Possibly there are three names of entry point:

  • index.js
  • app.js
  • server.js

If you don’t have any particular reason why the name of entry point should be that name, I recommend sticking to index.js. One reason you want to use server.js is when your project contains both server and client side codes, to avoid confusion whether the entry point is for backend or frontend. Nonetheless, client side codes are processed through Node packages, not by your application codebase. Therefore index.js is fine.

(If you want to distinguish server side code from client side code, separate the codes into server and client.)

File Name

There are two issues how to name your files.

The first issue is whether you prefix or suffix the file name with the folder name.

# Prefixing/suffixing
/controllers/user.controller.js
/models/user.model.js
/routers/user.router.js

/user/user.handlers.js
/user/user.model.js
/user/user.router.js

# Non-fixing
/controllers/user.js
/models/user.js
/routers/user.js

/user/handlers.js
/user/model.js
/user/router.js

As far as I found, the other blog posts on the structure of Express adapted the prefixing/suffixing strategy. I suspect the reason is that it is not good that two files exist in the same name. But don’t you think that’s why we sort files into folders? Let you be a judge.

The other issue is separators or casing. Again, as far as I found, the other blog posts adapts dot-separated file name like user.controller.js. The alternative naming could be hyphenation(user-controller.js) or camel casing(userController.js).

There are developers who make the first letter of orm file uppercase, like User.js, because orms are class.

Nest v.s. Flat

How deep can you nest folders? Let’s take a look at an example:

# Nest
/controllers/user/user.contoller.js
/controllers/user/friend/user.friend.contoller.js

/user/user.controller.js
/user/friend/user.friend.controller.js

# Flat
/controllers/user/user.contoller.js
/controllers/user/user.friend.contoller.js

/user/user.controller.js
/user/user.friend.controller.js

I believe nesting is more scalable. But that requires more thorough planning and organization. It would be much quicker to keep adding new files to one folder.

Routes v.s. Routers

I intentionally use the term router(s), not route(s). See Router from the official documentation.

The top-level express object has a Router() method that creates a new router object.

They use router. Imagine when you define routers or routes in code.

import { Router } from 'express';
const router = Router();
router.use('/path', …);

Why bother name one Router object a route?

Only situation that makes a sense to use route(s) is as follows:

function routes(app) {
	app.get('/', …);
	app.get('/user', …);
	app.get('/book', …);
	…

	return app;
}

Still, you should name the function like addRoutes.

Utils, Helpers, Library

The folder utils is often named as helpers or lib. Some project might have all of them. Yet, difference between utils, helpers, lib is ambiguous. Your project must contain only one of them. Or, have clear accordance among the team member which folder is for what.

Reference