Clean monolith code repo structure for a small JAM web app
Tags: webdev, javascript
Published: 2021-01-24 23:21:44

About a year ago I set out to start a new web app project, that would combine the uses of some of my favorite technologies that I've picked up over the past few years.

One of my main beefs with existing standards for modern web apps (typically in a server-side-rendered React stack) is how difficult the code repo's structure is to work with everyday. One of my specific goals in this project is to figure out a way to structure a web app, with both its backend and frontend, in a mono-repo (monolithic code repository), in a way that is easily understood and navigated.

I should emphasize that I also specifically picked to go with a JAM stack for this project, which means that this is certainly entirely not applicable for a server-side-rendered codebase (e.g. Next.js with SSR, etc.). The frontend is a statically-generated site.

The other emphasis I would make is that I'm going with a full Javascript codebase (so a frontend JS framework, with a node.js backend, instead of something like python or ruby). This allows me to share some code between backend and frontend, which is another goal of mine.

## Folder structure

![My JAM stack app folder structure](https://dev-to-uploads.s3.amazonaws.com/i/iwigfk8mnyc5saeeeyr6.png)

I figured the easiest way to understand the folder structure I want to describe is to visualize it as a tree from the code editor.

Immediately you'll notice a few things: There's only a simple layout of a `backend` folder, a `frontend` folder, and a small handful of other root level files and folders. We'll do a quick run-down of the important elements.

### Naming

You'll immediately notice that instead of going for more technically-focused terms like `client` or `server`, I opted to go with a very English-centric vocabulary for the folders, i.e. just `frontend` and `backend`.

I wanted there to be no thinking required to immediately figure out how the codebase is laid out, no matter how many years I've been away from the project and not remember anything.

Using terms like `client` or `server` always gives me a double-take. If I don't remember that this is a JAM stack app at all, `server` makes me question whether it has anything to do with a server-side-rendered code, the server of the frontend site, or if it would be the API backend. (or even some other server-side scripts or configuration) Using `backend` instead makes it clear that it is indeed the API backend, and not anything else.

### `backend`

As this is a JAM stack app, this is where the API server is. For my project, I chose to go with a [Fastify](https://www.fastify.io/) server. The main thing to point out here is what is contained within the `backend` folder. In short, imagine if you were building a backend-only project (Pick an [example Fastify project](https://github.com/fastify/fastify-example-todo) if you'd like). Simply, the entire repo of the backend project would belong within here, except `package.json`.

(Later we'll look at what the root level `package.json` contains and see how it is shared with frontend.)

### `frontend`

I'll just come out and say I'm not a fan of React. I chose Svelte for the project (and I believe I'll choose this for every personal web project going forward), but whether you choose Svelte, Vue or React, the same concept should apply here.

Similar to backend, imagine you were building a frontend-only project. The entire repo of the frontend project would belong within here, again except for `package.json`.

### `lib`

This is where backend and frontend shared modules are. In my case, I am building a simple card game. Some of the logic of the game belongs here. Later on, we'll take a look to see what a module that has different behavior on frontend and backend might look like here.

### `package.json`

This probably what ties it all up. The important things here to look at are the `scripts` section and the dependencies.

```js
  "scripts": {
    "check-format": "prettier --list-different './lib/**/*.js' './test/**/*.js'",
    "format": "prettier --write '*.js' './**/*.js'",
    "lint": "node ./node_modules/eslint/bin/eslint './lib/**/*.js' './test/**/*.js'",
    "test": "tap ./test/**/*.test.js",
    "backend": "node backend/index.js | pino-pretty -m message --ignore level,pid,hostname,v",
    "backend:raw": "node backend/index.js",
    "start": "node backend/index.js",
    "frontend": "node frontend/export-env.js && rollup -c -w",
    "frontend:serve": "sirv frontend/public",
    "build": "node frontend/export-env.js && rollup -c"
  },
```

The first four, `check-format`, `format`, `lint`, `test` are mostly standard scripts for use with any js projects. They are applicable for both frontend and backend source files. My tools of choice are `prettier`, `eslint` and `tap` which are pretty standard. I have a strong preference for `tap` over any other testing frameworks like `mocha` or `jest` for the simplicity.

The following scripts are more interesting. The `backend` scripts make it clear which part of the stack they are starting. The default `backend` starter script is what I use for development, and it pipes logs through `pino-pretty` to make it much easier to read. The generically named `start` script is to work with Heroku. I deploy my projects to Heroku so having the default server start script named `start` makes it a breeze to set up with no additional config needed.

The `frontend` scripts are similar: the main `frontend` script is what I use during local development (supports hot reload with `rollup`). The `build` step is for Netlify to run on deploy. The `export-env.js` is a small script that runs in order to take environmental variables I define on Netlify and write them into a file so the frontend bundle can use them. Needless to say this shouldn't contain any secret tokens or anything; but we can use these for any client-side public tokens we use (e.g. Google Analytics, etc.).

#### Dependencies

One notable thing here is that, every frontend-only package dependency in the project can simply be put in `devDependencies`. Because the app is a JAM stack one, the frontend is entirely statically-generated. The generation process happens during build-time, hence only `devDependencies` is needed.

For shared or backend-only dependencies, we would include the package in `dependencies`. This actually helps keep the list of `dependencies` very small for the backend app.

### `rollup.config.js` changes

A default rollup config file that comes with a sample Svelte/Sapper project is typically good to start, only having to update the path to the frontend app itself. In my case, the relevant lines in the rollup config look like this:

```js
{
  input: 'frontend/src/main.js',
  output: {
    sourcemap: true,
    format: 'iife',
    name: 'app',
    file: 'frontend/public/build/bundle.js'
  },
  plugins: [
    svelte({
      // enable run-time checks when not in production
      dev: !production,
      // we'll extract any component CSS out into
      // a separate file - better for performance
      css: css => {
        css.write('frontend/public/build/bundle.css');
      }
    }),
  ]
}
```

### `export-env.js` for frontend

As mentioned before, this file allows us to take Netlify-configured environmental variables and put them into the bundle. For safety reasons, we specify exactly which env vars we want to take (so we don't accidentally add secrets to the Netlify configuration and expose them to clients):

```js
'use strict';

/**
 * This script is for Netlify builds.
 * Netlify allows you to set up env vars, but they only exist at build time.
 * We run this as part of the build to generate a json file, which will then get
 * loaded for use in frontend.
 */

require('dotenv').config(); // for local builds with .env file

const fs = require('fs');

fs.writeFileSync(
  './frontend-env.js',
  `'use strict';\n\nmodule.exports = ${JSON.stringify({
    // The following list of env vars should match what's set on Netlify
    ENV: process.env.ENV,
    WEBSOCKET_URL: process.env.WEBSOCKET_URL
  })};\n`
);
```

### No `packages` folder, no separate npm packages, no lerna

This is really my big thing here. When I worked with a mono-repo web app at a job in the past, I *hated* the multiple packages setup with lerna. The `lerna bootstrap` process always takes way too long, and it was often error-prone, not knowing when you have to bootstrap again and such.

(And this is a small pet peeve of mine, but I hated how the root level of a frontend app using lerna would have a `package.json` and `packages` directory, which really messes with my tab-auto-complete on the command line.)

Here, *all* of my frontend code (Svelte components, or Vue/React components if you choose) is in `frontend`. Because I use Svelte and Rollup, I automatically get the benefit of tree-shakes, so even if my project grows large and I start having unused packages they won't pollute the bundle.

Because I'm not doing server-side-rendering, there is generally little "isomorphic" code. The only shared code I have, is explicitly and intentionally placed into the `lib` folder at root so I know they are shared; and there's no mistake. These are typically functional modules with logic only.

### Example shared module with different behavior on frontend and backend: `logger.js`

One of the modules in my `lib` folder is the logger module `logger.js`. The reason I don't just simply have separate logger modules in frontend and backend is that I want to be able to use the logger in other `lib` shared code themselves as well. My `logger.js` module code looks like this:

```js
'use strict';

const { ENV } = require('./constants');

if (typeof window === 'undefined') {
  // server: use pino
  const pino = require('pino');
  module.exports = pino({ timestamp: pino.stdTimeFunctions.isoTime });
} else {
  // client: logging only if dev
  const logFn = (level, obj) => {
    if (ENV === 'dev') {
      console.log(level, obj);
    }
  };
  module.exports = {
    fatal: obj => logFn('fatal', obj),
    error: obj => logFn('error', obj),
    warn: obj => logFn('warn', obj),
    info: obj => logFn('info', obj),
    debug: obj => logFn('debug', obj),
    trace: obj => logFn('trace', obj)
  };
}
```

There are several things going on here:

1. I use `typeof window === 'undefined'` to detect for the global window object to determine if this is backend or frontend.
1. On backend, I use the `pino` logger.
1. On frontend, only in a dev build, it uses `console.log`. Otherwise, it's a no-op.

## Conclusion

I'm not expecting this blog post to start a big movement away from the current startup best practice with file structures in mono-repo codebases. I'm merely introducing how I'm doing it, and perhaps you'd find it useful for your projects, find flaws and make improvements to it.

If there's any interests, I'd be happy to put together a sample project you can clone and run immediately with this structure, so you can play around with it more easily.