{ Production Directory Structure. }

Objectives:

By the end of this chapter, you should be able to:

  • Understand possible ways to structure Express.js apps for production
  • Learn how Common.js Modules works with larger apps
  • Navigate larger Node.js production codebases

Why do we care?

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!

Common.js Modules Revisited

Before we dive in, a couple key points about Common.js Modules.

  1. 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
    };
    
  2. 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
    
  3. 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

Structuring a Larger App

production directories

Root Directory

  • The root has all the "metadata" about the app. Everything not strictly part of the app code lives here, like global config settings, package.json, and node_modules.
  • Also at the root we have __tests__ for Jest tests (see next section)
  • All of the actual app code lives in the app directory

Main app Directory

  • The app contains two files at its root and all the other directories:
    1. app.js is where you initialize your app, database (potentially), routers, middleware, error handler, and global listener.
    2. config.js is code-specific config, for instance parsing environment variables or setting "global" variables across the app.

Route Handler Callbacks in 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
};

Helper Functions in 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.

Database / ORM Models in 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.

Express Routers in 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);

JSON Payloads in 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).

Example App Code

Check out Rithm 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

Continue

Creative Commons License