×
By the end of this chapter, you should be able to:
In most of the example code we've been writing, our code will not necessarily scale well. With Node/Express, you could absolutely write all of your routes, handler logic, database logic, helper functions, etc. in the single app.js
file. But if your app is sufficiently complicated, it will be thousands of lines and require constant searching. Worse, your file might be extremely prone to merge conflicts or other version control madness.
That said, the first question a beginner developer has when trying to divvy up the concerns is - how do I separate everything out? That's what we're going to answer here.
DISCLAIMER: our examples below refer to just one possible way of structuring things, which is no more or less correct than other interpretations. We'll give our reasons why we think this is a good structure, but please decide for yourself!
Before we dive in, a couple key points about Common.js Modules.
Most of the time, your files will export objects either as exports.whatever
or declaring module.exports
at the bottom:
module.exports = { firstFunc, secondFunc, thirdFunc };
You can reference the index.js
file of any directory just by requiring the directory name itself:
const handlers = require("./handlers"); // --> this points to ./handlers/index.js
For the above reason, in production codebases, most directories have index.js
files that collect the module.exports
from every file in their directories and export them all together. This allows you to just reference directories instead of specifying every single file you want something from.
In other words, use index.js
files as actual indexes for directories. The index files should export objects that export every exported function from every file in their directories.
That way, I know there exists a helper function called processDBError
, so I can just require it from wherever helpers
is:
const { processDBError } = require("../../helpers"); // destructure from the object exported by helpers/index.js
config
settings, package.json
, and node_modules
.__tests__
for Jest tests (see next section)app
directoryapp
Directoryapp.js
is where you initialize your app, database (potentially), routers, middleware, error handler, and global listener.config.js
is code-specific config, for instance parsing environment variables or setting "global" variables across the app.handlers
This directory is for the callback functions passed to Express's built-in route handlers (e.g. router.get
, app.get
, app.use
, router.route.get
, etc.).
We can separate the details of route handlers from the route declarations:
function fourOhFourHandler(request, response, next) { return next( new APIError( 404, "Resource Not Found", `${request.path} is not valid path to a Boilerplate API resource.` ) ); }
Several handlers can be listed in the same file and exported as an object:
module.exports = { fourOhFourHandler, fourOhFiveHandler, globalErrorHandler };
helpers
Helpers contain helper functions, classes, etc. that are ideally pure functions that are reused throughout the app. These functions have their own directory for reusability and testability.
const APIError = require("./APIError"); // this function can easily be unit-tested function processDBError(dbError) { let error = dbError; if (!(error instanceof APIError)) { error = new APIError( 500, error.name || "Internal Server Error", `Internal Database Error: ${error}` ); } return error; } module.exports = processDBError;
Each file contains one function that is the default export as well as the name of the file.
models
Models are the entities in the database using whatever ORM or database package you have. Preferably, much of the query logic can live inside the models as well. Instead of directly writing SQL queries in our routes files, we can abstract them to classes in the models folder.
Model file names generally are UpperCamelCase, non-plural to indicate that they represent DB entities, e.g. User
, Item
, Message
, etc.
routers
Each router file contains route declarations for a related collection of endpoints. It imports the handlers and references them as callbacks for the route methods:
const router = new express.Router(); // these come from the "handlers" directory const { createUser, readUser, updateUser, deleteUser } = userHandler; const { readUsers } = usersHandler; const { addUserFavorite, deleteUserFavorite } = favoritesHandler; router .route("") .get(authRequired, readUsers) // you can pass multiple handlers to a single route .post(createUser); router .route("/:username") .get(authRequired, readUser) .patch(authRequired, updateUser) .delete(authRequired, deleteUser);
schemas
The final major directory we would have is schemas
, which stores .json
files containing JSON Schemas used for validation (see the section of our curriculum on validation).
Check out Inf-Paces School's Hack-or-Snooze API Code to see this structure in play in the real world!
When you're ready, move on to Node Process Managers