WittCode💻

Bundling an Express App for Development and Production

By

Learn how to bundle a server side application for development and production. We will also go over the benefits of bundling server side applications by demonstrating with Express and Node.

Table of Contents 📖

Why Bundle Server Code

Bundling server side code is useful for reducing the size of the code on the server. This is particularly useful for serverless systems where, in AWS for example, AWS Lambdas are constantly being deployed or cold starting.

INFO: A cold start is the delay that occurs when a serverless function is invoked for the first time or after a prolonged idle period.

Another reason is that it reduces the amount of files that need to be managed. Often these bundles will consist of a single script file that can be ran on its own. A common, though now outdated, reason to not bundle server code is debugging. Bundling server side code used to make the debugging process more difficult. However, source maps in modern bundlers and languages negate this.

WARNING: Source maps give real stack traces and can be used to debug server code.

Project Initialization

npm init es6 -y
npm i express
npm i dotenv nodemon webpack webpack-cli

Webpack will bundle our Node application into a single file. We will then run this file with nodemon. In other words, Webpack will look for changes in the source code while nodemon will look for changes in the bundle.

Environment Variables

HOST=localhost
PORT=1234
URL=http://localhost:1234
HOST=localhost
PORT=1235
URL=http://localhost:1235

Project Code

import express from 'express';
import SayHello from './services/SayHello.js';
import WittCepterService from './services/WittCepterService.js';
import WittCodeService from './services/WittCodeService.js';
import CodeService from './services/CodeService.js';

const URL = process.env.URL;
const HOST = process.env.HOST;
const PORT = process.env.PORT;

const app = express();

app.get('/', (req, res) => {
  const sayHello = new SayHello();
  return res.send(sayHello.sayHello());
});

app.get('/wittcode', (req, res) => {
  const wittCodeService = new WittCodeService();
  return res.send(wittCodeService.sayHello());
});

app.get('/wittCepter', (req, res) => {
  const wittCepterService = new WittCepterService();
  return res.send(wittCepterService.sayHello());
});

app.get('/code', (req, res) => {
  const codeService = new CodeService();
  return res.send(codeService.sayHello());
});

app.get('/error', (req, res) => {
  throw new Error('Something went wrong');
});

app.listen(PORT, HOST, () => {
  console.log(`Running on ${URL}`);
});
import SayHello from "./SayHello.js";

export default class CodeService extends SayHello {
  constructor() {
    super('Code');
  }
}
export default class SayHello {
  name;

  constructor(name = 'WittCepter') {
    this.name = name;
  }

  sayHello() {
    return `${this.name} says hello!`;
  }
}
import SayHello from "./SayHello.js";

export default class WittCepterService extends SayHello {
  constructor() {
    super('WittCepter');
  }
}
import SayHello from "./SayHello.js";

export default class WittCodeService extends SayHello {
  constructor() {
    super('WittCode');
  }
}

Webpack Configurations

It is important that for both production and development we output source maps. This is so if anything goes wrong we can see what and where it went wrong in the original source code.

const path = require('path');
const webpack = require('webpack');
const dotenv = require('dotenv');

dotenv.config({path: path.resolve(__dirname, '.dev.env')});

module.exports = {
  mode: 'development',
  target: 'node',
  devtool: 'inline-source-map',
  entry:  './src/server.js',
  output: {
    path: path.resolve(__dirname, 'dist'),
    clean: true,
    filename: 'server.cjs',
  },
  plugins: [
    new webpack.EnvironmentPlugin(Object.keys(process.env)),
  ],
  ignoreWarnings: [
    {
      module: new RegExp('node_modules/express/lib/view.js'),
      message: /the request of a dependency is an expression/,
    },
  ],
};
const webpack = require('webpack');
const path = require('path');
const dotenv = require('dotenv');

dotenv.config({path: path.resolve(__dirname, '.prod.env')});

module.exports = {
  mode: 'production',
  target: 'node',
  devtool: 'source-map',
  entry: path.resolve(__dirname, 'src/server.js'),
  output: {
    path: path.resolve(__dirname, 'dist'),
    clean: true,
    filename: 'server.cjs'
  },
  plugins: [
    new webpack.EnvironmentPlugin(Object.keys(process.env))
  ],
  ignoreWarnings: [
    {
      module: new RegExp('node_modules/express/lib/view.js'),
      message: /the request of a dependency is an expression/,
    },
  ],
};

WARNING: We are ignorning a warning that comes from the express library. The node_modules folder will be added to the bundle by Webpack. In the view.js file of the express library a warning is issued that the request of a dependency is an expression. This is an issue that can be ignored. This error typically comes from dynamically importing a variable as opposed to a string.

npm Scripts

"scripts": {
  "start": "webpack --watch --config webpack.dev.cjs & nodemon --enable-source-maps .",
  "build": "webpack --config webpack.prod.cjs --stats-error-details && du -h dist/*"
}

The npm start command will create and run a development bundle with Webpack and nodemon. Specifically, Webpack will look for changes in our source code. When it detects some, it will rebuild the bundle. nodemon will look for changes in the bundle. When it detects some, it will restart the server.

ERROR: Make sure to provide the --enable-source-maps flag or the source maps created by Webpack will not work. The single & runs Webpack in the background and nodemon in the foreground.

Running the Code in Development

npm start

> start
> webpack --watch --config webpack.dev.cjs & nodemon --enable-source-maps .

[nodemon] 3.1.4
[nodemon] to restart at any time, enter `rs`
[nodemon] watching path(s): *.*
[nodemon] watching extensions: js,mjs,cjs,json
[nodemon] starting `node --enable-source-maps .`
Running on http://localhost:1234
asset server.cjs 1.77 MiB [compared for emit] (name: main)
runtime modules 793 bytes 4 modules
modules by path ./node_modules/ 766 KiB
  javascript modules 504 KiB 115 modules
  json modules 262 KiB
    modules by path ./node_modules/iconv-lite/ 86.7 KiB 8 modules
    ./node_modules/statuses/codes.json 1.51 KiB [built] [code generated]
    ./node_modules/mime/types.json 30.8 KiB [built] [code generated]
    ./node_modules/mime-db/db.json 143 KiB [built] [code generated]
modules by path ./src/ 1.59 KiB
  ./src/server.js 1.02 KiB [built] [code generated]
  ./src/services/SayHello.js 164 bytes [built] [code generated]
  ./src/services/WittCepterService.js 145 bytes [built] [code generated]
  ./src/services/WittCodeService.js 141 bytes [built] [code generated]
  ./src/services/CodeService.js 133 bytes [built] [code generated]
+ 15 modules
webpack 5.94.0 compiled successfully in 366 ms
curl localhost:1234/error
  
Error: Something went wrong
    at <anonymous> (webpack:///src/server.js:34:1)

Make changes in the source code and watch the live updates reflected in the console.

Running the Code in Production

npm run build

> build
> webpack --config webpack.prod.cjs --stats-error-details && du -h dist/*

asset server.cjs 587 KiB [emitted] [minimized] (name: main) 2 related assets
orphan modules 583 bytes [orphan] 4 modules
runtime modules 211 bytes 2 modules
cacheable modules 767 KiB
  javascript modules 505 KiB 115 modules
  json modules 262 KiB
    modules by path ./node_modules/iconv-lite/ 86.7 KiB
      ./node_modules/iconv-lite/encodings/tables/shiftjis.json 8.78 KiB [built] [code generated]
      ./node_modules/iconv-lite/encodings/tables/eucjp.json 15.1 KiB [built] [code generated]
      ./node_modules/iconv-lite/encodings/tables/cp936.json 20.1 KiB [built] [code generated]
      ./node_modules/iconv-lite/encodings/tables/gbk-added.json 909 bytes [built] [code generated]
      + 4 modules
    ./node_modules/statuses/codes.json 1.51 KiB [built] [code generated]
    ./node_modules/mime/types.json 30.8 KiB [built] [code generated]
    ./node_modules/mime-db/db.json 143 KiB [built] [code generated]
+ 16 modules
webpack 5.94.0 compiled successfully in 1351 ms
588K    dist/server.cjs
8.0K    dist/server.cjs.LICENSE.txt
784K    dist/server.cjs.map

Note how the application is only 588 kilobytes in size. Lets look at the size differences if we didn't bundle the application.

du -h -d 1 . 
1.3M    ./dist
3.4M    ./node_modules
 20K    ./src
4.9M    .

INFO: Note that we want to run an npm install with development packages omitted to mimick the production environment that Webpack creates.

Now we would just need to run our server.cjs bundle file with source maps enabled.

node --enable-source-maps dist/server.cjs
Running on http://localhost:1235
curl localhost:1235/error
Error: Something went wrong
    at fn (webpack:///src/server.js:34:9)
Bundling an Express App for Development and Production