Setting Up an Express App

I’ve used Express frequently enough that I am now fairly quick at setting up Express for development and production. Today I wish to share this setup process with you so that I can be mocked and ridiculed for my naivety. Is this imposter syndrome? Maybe. Do I care? Kind of. Hold on, I need to psych myself up.

Rap god meme - I'm beginnin' to feel like a dev god, dev god

If you want to learn more about starting with Express, there is a lengthy tutorial on MDN that covers most of the tools that I will present here. Now that that’s out of the way, let’s dive in.

Generating the App Skeleton

To generate the skeleton of the Express app, you will want to install the express-generator package globally. I will be using pnpm because of its improvements upon npm. You can install pnpm here. Once pnpm is installed, use this command in a terminal to install the express-generator package globally:

pnpm add -g express-generator

Then enter the folder where you want the Express app to be generated and do this command:

express --view={view_engine}

The curly brackets, {}, indicate a variable. Set the view_engine variable to the name of the template engine of your choice (I would suggest pug or ejs) unless you are building an API where server-side rendering is not required; in this case, you can do this:

express --no-view

Now you can install project packages:

pnpm install
pnpm up --latest

Express generator is a little outdated, so once you generate the project, you may want to remove its use of the var keyword. Use a replace-all function that your IDE provides to replace all instances of var with const.

Configuring package.json

If another contributor would need a package to run your project, you should make sure to install it locally instead of globally like with the express-generator package. With this in mind, install the nodemon package as a local dev dependency with:

pnpm add -D nodemon

Make sure to remain within your project’s directory in the terminal to ensure that your project is installing the package. The nodemon package is important for speeding up development, as it enables automatic server restarts whenever file changes occur. With nodemon installed, you can create the most rudimentary scripts that your app should have in the scripts block of the package.json file:

"scripts": {
	"start": "node ./bin/www",
	"watch_node": "nodemon --ext js",
	"dev": "DEBUG=$npm_package_name:* pnpm watch_node",
	"preinstall": "npx only-allow pnpm"
}

Copy this block and replace the current scripts block in your package.json file. Note that these scripts were designed for a Linux system, so you may need to do some tweaking if you are running a different operating system. To run your app in development, enter this into the terminal:

pnpm dev

To run your app in production, enter this into the terminal:

pnpm start

By default, your app will be available at localhost:3000. Enter this into your browser to visualize it.

Once your project comes further along, you will likely want to run concurrent processes in development. Let’s say you want nodemon to restart the dev server when js or pug files are changed, sass to monitor your sass files and to recompile to CSS on change, and webpack to monitor all files belonging to your JavaScript bundle and to re-bundle them on change. The resulting scripts block could look like this:

"scripts": {
	"start": "node ./bin/www",
	"watch_node": "nodemon --ext js,pug",
	"watch_scss": "sass --no-source-map --watch ./public/stylesheets/style.scss ./public/stylesheets/style.css",
	"watch_webpack": "webpack --watch",
	"dev": "DEBUG=$npm_package_name:* pnpm watch_node & pnpm watch_scss & pnpm 	watch_webpack",
	"preinstall": "npx only-allow pnpm"
},

Note that there is a difference between & and &&: & runs processes concurrently, while && runs processes sequentially. Since the watch scripts are continuous processes, the use of && in the dev script would result in only watch_node being run. The use of & here works, but you may find that node package concurrently does a better job of displaying concurrent processes in the terminal.

ENV File

The env file is used for defining your environment variables. These variables often hold sensitive information like passwords or API keys, so you should make sure that you never push this file to GitHub for the world to see. To create one, add a new file named .env to the root of your project. To start, you can define the preferred port that you want your app to be hosted on, as well as the node environment variable, NODE_ENV:

PORT = '3000'
NODE_ENV = 'development'

If you go to the file at ./bin/www, you will see this section:

/**
* Get port from environment and store in Express.
*/

const port = normalizePort(process.env.PORT || '3000');
app.set('port', port);

The process.env.PORT call is an example of how to use environment variables defined in the .env file. Simply prepend the environment variable with process.env and you will be able to call it anywhere in the project, assuming one condition is met: you have to require the dotenv package in your app.js file. Install the package via

pnpm add dotenv

Then in app.js, add this at the top of the file:

require("dotenv").config()

Your environment variables are now set up. In the next step, we look at how to prevent an accidental push of your .env file to GitHub.

Git

Git is a version control system used to save your progress as you go. You may not think it is worth using if your project is small, but it becomes more valuable as your codebase increases in size. Even if your project is small, Git is a valuable skill to have, and you should consider using it for the sake of learning. Once you have set up Git on your system, initialize Git in your project via

git init

Now before you tell Git to start tracking all of your files, you will want to ensure that Git ignores some files, such as those in the node_modules folder and your .env file. To do this, make a new file named .gitignore at the root of the project. Then open the file and add these lines:

node_modules
.env

Git will now ignore these files for future commits. Now you can tell Git to start tracking all other files using these commands:

git add .
git commit -m “Init commit”

The first command stages all changes to the project (in this case, the addition of the all files except those ignored), and the second command saves the changes with a message. Git is a lot more complex than this, so make sure to read up on what it is capable of.

ESLint and Prettier

As you code, it is best to stick to a coding standard to make your code more readable and maintainable. ESLint makes it easy to do this by flagging code that does not comply with the given standard. On the other hand there is Prettier, which is like ESLint except that it deals specifically with formatting. These are both important packages that should be used frequently to keep your codebase tidy. The setup is fairly straightforward, so I will not bog you down with too many details. However, I would be remiss if I were to not mention eslint-config-prettier as well.  ESLint and Prettier do not always play nice due to a lack of coordination, so eslint-config-prettier is used to correct this. 

The App Factory

In the Express app, you will see that an app.js file was generated by express-generator. At first you will be fine with app.js having a structure similar to its current state, but once you add production-only features to your app, you will want your app to act differently depending on whether the environment is development or production. To manage the environment’s state, you can use the NODE_ENV environment variable found in the .env file, as you will see below.

The goal is to put everything in the app.js file into a factory function and have the function return an Express app tailored to the current environment type. I don’t really believe in pulling punches, so I’m just going to throw out the final product right here.

Rap god meme - Lyrics coming at you at supersonic speed

require("dotenv").config()
const liveReload = require("livereload")
const connectLiveReload = require("connect-livereload")
const RateLimit = require("express-rate-limit")
const compression = require("compression")
const helmet = require("helmet")
const crypto = require("crypto")
const createError = require("http-errors")
const express = require("express")
const path = require("path")
const logger = require("morgan")
const session = require("express-session")
const MemoryStore = require("memorystore")(session)
const passport = require("./utils/auth")
const flash = require("connect-flash")

function App() {
  const isProd = process.env.NODE_ENV === "production"
  const isDev = process.env.NODE_ENV === "development"
  /* Dev env variables */
  const autologin = true
  
  const app = express()

  if (isProd) {
    /* Security Setup */
    app.use((req, res, next) => {
      const nonce = crypto.randomBytes(16).toString("base64")
      res.locals.nonce = nonce

      helmet({
        contentSecurityPolicy: {
          directives: {
            defaultSrc: ["'self'"],
            scriptSrc: ["'self'", `'nonce-${nonce}'`],
          },
        },
      })(req, res, next)
    })

    /* Rate limiting */
    app.use(
      // 50 requests per minute
      RateLimit({
        windowMs: 1 * 60 * 1000,
        max: 50,
      }),
    )

    /* Response compression */
    app.use(compression())
  }

  /* Authentication Setup */
  app.use(
    session({
      secret: process.env.SESSION_SECRET,
      resave: false,
      saveUninitialized: true,
      cookie: {
        sameSite: "lax",
      },
      store: new MemoryStore({
        checkPeriod: 24 * 60 * 60 * 1000, // 24 hours in ms
      }),
    }),
  )

  app.use(passport.initialize())
  app.use(passport.session())
  app.use(flash())

  if (isDev) {
    /* Live Reload Setup */
    const liveReloadServer = liveReload.createServer()
    liveReloadServer.watch(path.join(__dirname, "public"))
    liveReloadServer.server.once("connection", () => {
      setTimeout(() => {
        liveReloadServer.refresh("/")
      }, 200)
    })

    app.use(connectLiveReload())

    /* Autologin */
    if (autologin) {
      let initAutologin = true

      app.use(async (req, res, next) => {
        if (initAutologin) {
          initAutologin = false

          // Get autologUser from database here

          req.login(autologUser, (err) => {
            if (err) {
              return next(err)
            }
          })
        }

        next()
      })
    }
  }

  /* View Engine Setup */
  app.set("views", path.join(__dirname, "views"))
  app.set("view engine", "pug")

  // Add user locals
  app.use(async (req, res, next) => {
    if (!req.user) {
      return next()
    }

    res.locals.loginUser = req.user

    // ...other user locals

    next()
  })

  /* Miscellaneous Setup */
  app.use(logger("dev"))
  app.use(express.json())
  app.use(express.urlencoded({ extended: false }))

  /* Static Setup */
  app.use(express.static(path.join(__dirname, "public")))

  /* Route Setup */
  const indexRouter = require("./routes/index")
  const usersRouter = require('./routes/users')

  app.use("/", indexRouter)
  app.use('/users', usersRouter);

  /* Error Handling */
  app.use(function (req, res, next) {
    next(createError(404))
  })

  app.use(function (err, req, res, next) {
    res.locals.message = err.message;
    res.locals.error = isDev ? err : {};

    // render the error page
    res.status(err.status || 500);
    res.render('error');
  })

  return app
}

module.exports = App

Meme of man blinking as if at a loss for words

First we will go over the core features (neither strictly development nor strictly production), then the development features, and finally the production features. You can see how some parts of the app are strictly development or production by their placement within conditional statements checking the values of variables isProd or isDev.

Core Features

Authentication

/* Authentication Setup */
app.use(
  session({
    secret: process.env.SESSION_SECRET,
    resave: false,
    saveUninitialized: true,
    cookie: {
      sameSite: "lax",
    },
    store: new MemoryStore({
      checkPeriod: 24 * 60 * 60 * 1000, // 24 hours in ms
    }),
  }),
)

app.use(passport.initialize())
app.use(passport.session())
app.use(flash())

First you will notice authentication middleware using the express-session and passport packages. This enables users to remain logged in, and as long as a user is logged in, passport attaches a user object to the request object to simplify access to the user’s information throughout the following middleware. You may have noticed that the session function requires the use of an environment variable, SESSION_SECRET. To define this variable, go to the terminal and use this command:

openssl rand -base64 32

Then paste the output as the value of the SESSION_SECRET in your .env file.

For whatever reason, express-session comes with a development-only memory store; this is where the memorystore package comes in. Also, the connect-flash package was the simplest solution I could find to display passport’s authentication messages. I won’t delve into the details here, but it will likely take some experimentation to get passport working properly.

View Engine

/* View Engine Setup */
app.set("views", path.join(__dirname, "views"))
app.set("view engine", "pug")

This section includes the declaration of the view engine (e.g. Pug) and the path to the views folder. If you are making an API (meaning a frontend is not necessary), you should remove this section.

Template Locals

// Add user locals
app.use(async (req, res, next) => {
  if (!req.user) {
    return next()
  }

  res.locals.loginUser = req.user

  // ...other user locals

  next()
})

Template locals are defined by adding a key-value pair to the res.locals object. The reason for defining locals is that they become available as variables in your views. Notice how user locals are only defined if a user is logged in, which can be determined by checking for the existence of the req.user value. You can define as many locals as you want, just make sure that you have a strategy to avoid overwriting locals that are already defined.

Miscellaneous Middleware

/* Miscellaneous Setup */
app.use(logger("dev"))
app.use(express.json())
app.use(express.urlencoded({ extended: false }))

The first middleware is created using the morgan package which logs HTTP requests in the terminal. If you were to visit a page that used a CSS file and a JavaScript bundle, then morgan could log something like this:

GET /stylesheets/style.css 304 1.348 ms - -
GET / 200 3164.760 ms - -
GET /javascripts/bundle.js 304 0.482 ms - -

Next up is the express.json() and express.urlencoded() middleware. Both are used to parse incoming request bodies from POST/PUT requests, where parsed results are place in the req.body object. The difference is that express.json() parses data encoded in JSON while express.urlencoded() parses data encoded in url encoding.

Static Files

/* Static Setup */
app.use(express.static(path.join(__dirname, "public")))

Static files are typically stored in your public folder. Examples of static files include images, CSS files, and JS files. Note that a request is made for each static file required by an HTML page, so you may want to use a bundler like Webpack to minimize the number of requests.

Routes

/* Route Setup */
const indexRouter = require("./routes/index")
const usersRouter = require('./routes/users')

app.use("/", indexRouter)
app.use('/users', usersRouter);

The final app-level destination of a non-404 request is a router, where a router is used to process a request if the path it is paired with matches the request url. Note that route (router + path) order matters, as Express uses the first route with a matching path.

Error Handling

/* Error Handling */
app.use(function (req, res, next) {
  next(createError(404))
})

app.use(function (err, req, res, next) {
  res.locals.message = err.message;
  res.locals.error = isDev ? err : {};

  // render the error page
  res.status(err.status || 500);
  res.render('error');
})

If all routers fail to have a path matching the request url, the requested url should be deemed invalid and given a 404 error page. You can see how the first middleware after all of the routes generates a 404 error and passes it on to the following error handling middleware. If an error had occurred in one of the routes, that error would be passed to the error handling middleware instead. Read more about error handling in Express here.

Development Features

Livereload

/* Live Reload Setup */
const liveReloadServer = liveReload.createServer()
liveReloadServer.watch(path.join(__dirname, "public"))
liveReloadServer.server.once("connection", () => {
  setTimeout(() => {
    liveReloadServer.refresh("/")
  }, 200)
})

app.use(connectLiveReload())

The livereload package complements nodemon nicely: nodemon restarts the server on change, and livereload refreshes the browser. Of course, connect-livereload is also needed to obtain this functionality.

There are a couple other things to note. First, you may need to adjust the time taken by the timeout for proper browser refreshes. Second, notice how livereload is set up to only watch the public folder. This is coupled with the existence of a nodemon.json file in the project’s root directory with these contents:

{
  "ignore": ["public"]
}

The server does not need resetting on static file change but the browser does need to be refreshed to refetch static files, and the server needs to be reset on other file change but livereload already refreshes the browser whenever the server restarts. Therefore, livereload watches the public folder while nodemon watches everything else.

Autologin

/* Autologin */
if (autologin) {
  let initAutologin = true

  app.use(async (req, res, next) => {
    if (initAutologin) {
      initAutologin = false

      // Get autologUser from database here

      req.login(autologUser, (err) => {
        if (err) {
          return next(err)
        }
      })
    }

    next()
  })
}

In development, you may find that you are having to log a test user in constantly, since you are logged out every time the server restarts due to nodemon. To get around this, you can sign the user in automatically using the login method on the request object provided by the passport package. Since this process can be taxing on the speed at which your development server can respond, it is best to only allow an automatic login on the first request to a newly started server, if at all. The initAutologin variable ensures that the automatic login middleware only runs fully once, and if no automatic login is needed at all, simply change the value of the autologin constant defined at the beginning of the app function.

Production Features

Nonce and CSP

/* Security Setup */
app.use((req, res, next) => {
  const nonce = crypto.randomBytes(16).toString("base64")
  res.locals.nonce = nonce

  helmet({
    contentSecurityPolicy: {
      directives: {
        defaultSrc: ["'self'"],
        scriptSrc: ["'self'", `'nonce-${nonce}'`],
      },
    },
  })(req, res, next)
})

nonce is a randomly generated number attached to inline scripts to prevent cross-site scripting. If you need one in one of your views, generate it with the crypto package built into Node.js and make it a template local. A CSP (content security policy) is another important aspect of security, and the helmet package makes setting one up easy.

Rate Limiting Response

/* Rate limiting */
app.use(
  // 50 requests per minute
  RateLimit({
    windowMs: 1 * 60 * 1000,
    max: 50,
  }),
)

This is an obvious production necessity. Unless you want to be vulnerable to DOS (denial of service) attacks, I suggest you look into rate limiting.

Compression

/* Response compression */
app.use(compression())

This is another simple improvement that can speed up your app in production by decreasing the size of the response body.

Conclusion

Now you know how to set up an Express app! Just remember to change the NODE_ENV environment variable to production/development when necessary. And with that I am one step closer to being able to rap like a computer.

Comments