Creating a Node Express-Webpack App with Dev and Prod Builds

Binyamin
Binyamin
May 24, 2018 · 17 min read

What We Want To Make

Our Tech Stack

Ok, Let’s Begin…! 🤠

Step 1: The Express Server

mkdir express-webpack
cd express-webpack
npm init -y
npm install --save express
"scripts": {
"start": "node ./server.js"
},
const path = require('path')
const express = require('express')
const app = express(),
DIST_DIR = __dirname,
HTML_FILE = path.join(DIST_DIR, 'index.html')
app.use(express.static(DIST_DIR))app.get('*', (req, res) => {
res.sendFile(HTML_FILE)
})
const PORT = process.env.PORT || 8080
app.listen(PORT, () => {
console.log(`App listening to ${PORT}....`)
console.log('Press Ctrl+C to quit.')
})
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Express and Webpack App</title>
<link rel="shortcut icon" href="#">
</head>
<body>
<h1>Expack</h1>
<p class="description">Express and Webpack Boilerplate App</p>
</body>
</html>

Step 2: Install and Enable Webpack

npm install --save-dev webpack webpack-cli webpack-node-externals
npm install --save-dev @babel/core @babel/preset-env babel-loader
# npx lets you run babel-upgrade without installing it locally
npx babel-upgrade --write
npm install --save-dev html-loader html-webpack-plugin
const path = require('path')
const webpack = require('webpack')
const nodeExternals = require('webpack-node-externals')
const HtmlWebPackPlugin = require("html-webpack-plugin")
module.exports = {
entry: {
server: './server.js',
},
output: {
path: path.join(__dirname, 'dist'),
publicPath: '/',
filename: '[name].js'
},
target: 'node',
node: {
// Need this when working with express, otherwise the build fails
__dirname: false, // if you don't put this is, __dirname
__filename: false, // and __filename return blank or /
},
externals: [nodeExternals()], // Need this to avoid error when working with Express
module: {
rules: [
{
// Transpiles ES6-8 into ES5
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: "babel-loader"
}
},
{
// Loads the javacript into html template provided.
// Entry point is set below in HtmlWebPackPlugin in Plugins
test: /\.html$/,
use: [{loader: "html-loader"}]
}
]
},
plugins: [
new HtmlWebPackPlugin({
template: "./index.html",
filename: "./index.html",
excludeChunks: [ 'server' ]
})
]
}
import path from 'path'
import express from 'express'
const app = express(),
DIST_DIR = __dirname,
HTML_FILE = path.join(DIST_DIR, 'index.html')
app.use(express.static(DIST_DIR))app.get('*', (req, res) => {
res.sendFile(HTML_FILE)
})
const PORT = process.env.PORT || 8080
app.listen(PORT, () => {
console.log(`App listening to ${PORT}....`)
console.log('Press Ctrl+C to quit.')
})
{
'presets': ['@babel/preset-env']
}
"scripts": {
"build": "rm -rf dist && webpack --mode development",
"start": "node ./dist/server.js"
},

Step 3: Add CSS and Javascript Functionality to App

npm install --save-dev css-loader file-loader style-loader
.babelrc
.git
.gitignore
README.md
dist
node_modules
package-lock.json
package.json
webpack.config.js
webpack.server.config.js
src
index.js
html
index.html
css
style.css
js
index.js
img
awful-selfie.jpg
server
server.js
"scripts": {
"build": "rm -rf dist && webpack --mode development --config webpack.server.config.js && webpack --mode development",
"start": "node ./dist/server.js"
},
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Express and Webpack App</title>
<link rel="shortcut icon" href="#">
</head>
<body>
<h1>Expack</h1>
<p class="description">Express and Webpack Boilerplate App</p>
<div class="awful-selfie"></div>
</body>
</html>
h1, h2, h3, h4, h5, p {
font-family: helvetica;
color: #3e3e3e;
}
.description {
font-size: 14px;
color: #9e9e9e;
}
.awful-selfie{
background: url(../img/bg.jpg);
width: 300px;
height: 300px;
background-size: 100% auto;
background-repeat: no-repeat;
}
import logMessage from './js/logger'
import './css/style.css'
// Log message to console
logMessage('Welcome to Expack!')
const logMessage = msg => console.log(msg)export default logMessage
const path = require('path')
const webpack = require('webpack')
const nodeExternals = require('webpack-node-externals')
module.exports = {
entry: {
server: './src/server/server.js',
},
output: {
path: path.join(__dirname, 'dist'),
publicPath: '/',
filename: '[name].js'
},
target: 'node',
node: {
// Need this when working with express, otherwise the build fails
__dirname: false, // if you don't put this is, __dirname
__filename: false, // and __filename return blank or /
},
externals: [nodeExternals()], // Need this to avoid error when working with Express
module: {
rules: [
{
// Transpiles ES6-8 into ES5
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: "babel-loader"
}
}
]
}
}
const path = require("path")
const webpack = require('webpack')
const HtmlWebPackPlugin = require("html-webpack-plugin")
module.exports = {
entry: {
main: './src/index.js'
},
output: {
path: path.join(__dirname, 'dist'),
publicPath: '/',
filename: '[name].js'
},
target: 'web',
devtool: 'source-map',
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
loader: "babel-loader",
},
{
// Loads the javacript into html template provided.
// Entry point is set below in HtmlWebPackPlugin in Plugins
test: /\.html$/,
use: [
{
loader: "html-loader",
//options: { minimize: true }
}
]
},
{
test: /\.css$/,
use: [ 'style-loader', 'css-loader' ]
},
{
test: /\.(png|svg|jpg|gif)$/,
use: ['file-loader']
}
]
},
plugins: [
new HtmlWebPackPlugin({
template: "./src/html/index.html",
filename: "./index.html",
excludeChunks: [ 'server' ]
})
]
}

Step 4: Seperate App into Dev and Prod Builds

src
server
server-dev.js
server-prod.js
webpack.dev.config.js
webpack.prod.config.js
webpack.server.config.js
"scripts": {
"buildDev": "rm -rf dist && webpack --mode development --config webpack.server.config.js && webpack --mode development --config webpack.dev.config.js",
"buildProd": "rm -rf dist && webpack --mode production --config webpack.server.config.js && webpack --mode production --config webpack.prod.config.js",
"start": "node ./dist/server.js"
},
const path = require('path')
const webpack = require('webpack')
const nodeExternals = require('webpack-node-externals')
module.exports = (env, argv) => {
const SERVER_PATH = (argv.mode === 'production') ?
'./src/server/server-prod.js' :
'./src/server/server-dev.js'
return ({
entry: {
server: SERVER_PATH,
},
output: {
path: path.join(__dirname, 'dist'),
publicPath: '/',
filename: '[name].js'
},
target: 'node',
node: {
// Need this when working with express, otherwise the build fails
__dirname: false, // if you don't put this is, __dirname
__filename: false, // and __filename return blank or /
},
externals: [nodeExternals()], // Need this to avoid error when working with Express
module: {
rules: [
{
// Transpiles ES6-8 into ES5
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: "babel-loader"
}
}
]
}
})
}
npm install --save-dev mini-css-extract-plugin uglifyjs-webpack-plugin optimize-css-assets-webpack-plugin url-loader
const path = require("path")
const HtmlWebPackPlugin = require("html-webpack-plugin")
const MiniCssExtractPlugin = require("mini-css-extract-plugin")
const UglifyJsPlugin = require("uglifyjs-webpack-plugin")
const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin");
module.exports = {
entry: {
main: './src/index.js'
},
output: {
path: path.join(__dirname, 'dist'),
publicPath: '/',
filename: '[name].js'
},
target: 'web',
devtool: 'source-map',
// Webpack 4 does not have a CSS minifier, although
// Webpack 5 will likely come with one
optimization: {
minimizer: [
new UglifyJsPlugin({
cache: true,
parallel: true,
sourceMap: true // set to true if you want JS source maps
}),
new OptimizeCSSAssetsPlugin({})
]
},
module: {
rules: [
{
// Transpiles ES6-8 into ES5
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: "babel-loader"
}
},
{
// Loads the javacript into html template provided.
// Entry point is set below in HtmlWebPackPlugin in Plugins
test: /\.html$/,
use: [
{
loader: "html-loader",
options: { minimize: true }
}
]
},
{
// Loads images into CSS and Javascript files
test: /\.jpg$/,
use: [{loader: "url-loader"}]
},
{
// Loads CSS into a file when you import it via Javascript
// Rules are set in MiniCssExtractPlugin
test: /\.css$/,
use: [MiniCssExtractPlugin.loader, 'css-loader']
},
]
},
plugins: [
new HtmlWebPackPlugin({
template: "./src/html/index.html",
filename: "./index.html"
}),
new MiniCssExtractPlugin({
filename: "[name].css",
chunkFilename: "[id].css"
})
]
}
const path = require("path")
const webpack = require('webpack')
const HtmlWebPackPlugin = require("html-webpack-plugin")
module.exports = {
entry: {
main: './src/index.js'
},
output: {
path: path.join(__dirname, 'dist'),
publicPath: '/',
filename: '[name].js'
},
target: 'web',
devtool: 'source-map',
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
loader: "babel-loader",
},
{
// Loads the javacript into html template provided.
// Entry point is set below in HtmlWebPackPlugin in Plugins
test: /\.html$/,
use: [
{
loader: "html-loader",
//options: { minimize: true }
}
]
},
{
test: /\.css$/,
use: [ 'style-loader', 'css-loader' ]
},
{
test: /\.(png|svg|jpg|gif)$/,
use: ['file-loader']
}
]
},
plugins: [
new HtmlWebPackPlugin({
template: "./src/html/index.html",
filename: "./index.html",
excludeChunks: [ 'server' ]
})
]
}

Step 5: Add Webpack Dev Middleware

npm install --save-dev webpack-dev-middleware
const path = require('path')
const webpack = require('webpack')
const HtmlWebPackPlugin = require('html-webpack-plugin')
module.exports = {
entry: {
main: './src/index.js'
},
output: {
path: path.join(__dirname, 'dist'),
publicPath: '/',
filename: '[name].js'
},
mode: 'development',
target: 'web',
devtool: 'source-map',
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
loader: "babel-loader",
},
{
// Loads the javacript into html template provided.
// Entry point is set below in HtmlWebPackPlugin in Plugins
test: /\.html$/,
use: [
{
loader: "html-loader",
//options: { minimize: true }
}
]
},
{
test: /\.css$/,
use: [ 'style-loader', 'css-loader' ]
},
{
test: /\.(png|svg|jpg|gif)$/,
use: ['file-loader']
}
]
},
plugins: [
new HtmlWebPackPlugin({
template: "./src/html/index.html",
filename: "./index.html",
excludeChunks: [ 'server' ]
}),
new webpack.NoEmitOnErrorsPlugin()
]
}
import path from 'path'
import express from 'express'
import webpack from 'webpack'
import webpackDevMiddleware from 'webpack-dev-middleware'
import config from '../../webpack.dev.config.js'
const app = express(),
DIST_DIR = __dirname,
HTML_FILE = path.join(DIST_DIR, 'index.html'),
compiler = webpack(config)
app.use(webpackDevMiddleware(compiler, {
publicPath: config.output.publicPath
}))
app.get('*', (req, res, next) => {
compiler.outputFileSystem.readFile(HTML_FILE, (err, result) => {
if (err) {
return next(err)
}
res.set('content-type', 'text/html')
res.send(result)
res.end()
})
})
const PORT = process.env.PORT || 8080app.listen(PORT, () => {
console.log(`App listening to ${PORT}....`)
console.log('Press Ctrl+C to quit.')
})

Step 6: Add Hot Module Reloading

npm install --save-dev webpack-hot-middleware
const path = require('path')
const webpack = require('webpack')
const HtmlWebPackPlugin = require('html-webpack-plugin')
module.exports = {
entry: {
main: ['webpack-hot-middleware/client?path=/__webpack_hmr&timeout=20000', './src/index.js']
},
output: {
path: path.join(__dirname, 'dist'),
publicPath: '/',
filename: '[name].js'
},
mode: 'development',
target: 'web',
devtool: 'source-map',
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
loader: "babel-loader",
},
{
// Loads the javacript into html template provided.
// Entry point is set below in HtmlWebPackPlugin in Plugins
test: /\.html$/,
use: [
{
loader: "html-loader",
//options: { minimize: true }
}
]
},
{
test: /\.css$/,
use: [ 'style-loader', 'css-loader' ]
},
{
test: /\.(png|svg|jpg|gif)$/,
use: ['file-loader']
}
]
},
plugins: [
new HtmlWebPackPlugin({
template: "./src/html/index.html",
filename: "./index.html",
excludeChunks: [ 'server' ]
}),
new webpack.HotModuleReplacementPlugin(),
new webpack.NoEmitOnErrorsPlugin()
]
}
import path from 'path'
import express from 'express'
import webpack from 'webpack'
import webpackDevMiddleware from 'webpack-dev-middleware'
import webpackHotMiddleware from 'webpack-hot-middleware'
import config from '../../webpack.dev.config.js'
const app = express(),
DIST_DIR = __dirname,
HTML_FILE = path.join(DIST_DIR, 'index.html'),
compiler = webpack(config)
app.use(webpackDevMiddleware(compiler, {
publicPath: config.output.publicPath
}))
app.use(webpackHotMiddleware(compiler))app.get('*', (req, res, next) => {
compiler.outputFileSystem.readFile(HTML_FILE, (err, result) => {
if (err) {
return next(err)
}
res.set('content-type', 'text/html')
res.send(result)
res.end()
})
})
const PORT = process.env.PORT || 8080app.listen(PORT, () => {
console.log(`App listening to ${PORT}....`)
console.log('Press Ctrl+C to quit.')
})
import logMessage from './js/logger'
import './css/style.css'
// Log message to console
logMessage('A very warm welcome to Expack!')
// Needed for Hot Module Replacement
if(typeof(module.hot) !== 'undefined') {
module.hot.accept() // eslint-disable-line no-undef
}

Step 7: Add ESLint Code Linting

npm install --save-dev eslint babel-eslint eslint-loader
module.exports = {
"plugins": [ "react" ],
"extends": [
"eslint:recommended",
"plugin:react/recommended"
],
"parser": "babel-eslint"
};
const path = require('path')
const webpack = require('webpack')
const HtmlWebPackPlugin = require('html-webpack-plugin')
module.exports = {
entry: {
main: ['webpack-hot-middleware/client?path=/__webpack_hmr&timeout=20000', './src/index.js']
},
output: {
path: path.join(__dirname, 'dist'),
publicPath: '/',
filename: '[name].js'
},
mode: 'development',
target: 'web',
devtool: 'source-map',
module: {
rules: [
{
enforce: "pre",
test: /\.js$/,
exclude: /node_modules/,
loader: "eslint-loader",
options: {
emitWarning: true,
failOnError: false,
failOnWarning: false
}
},
{
test: /\.js$/,
exclude: /node_modules/,
loader: "babel-loader",
},
{
// Loads the javacript into html template provided.
// Entry point is set below in HtmlWebPackPlugin in Plugins
test: /\.html$/,
use: [
{
loader: "html-loader",
//options: { minimize: true }
}
]
},
{
test: /\.css$/,
use: [ 'style-loader', 'css-loader' ]
},
{
test: /\.(png|svg|jpg|gif)$/,
use: ['file-loader']
}
]
},
plugins: [
new HtmlWebPackPlugin({
template: "./src/html/index.html",
filename: "./index.html",
excludeChunks: [ 'server' ]
}),
new webpack.HotModuleReplacementPlugin(),
new webpack.NoEmitOnErrorsPlugin()
]
}
module.hot.accept() // eslint-disable-line no-undef

Step 8: Add Jest Unit Testing and Coverage

npm install --save-dev jest
src
dist
__mocks__
fileMock.js
styleMock.js
module.exports = 'test-file-stub';
module.exports = {};
"scripts": {
"test": "jest",
"coverage": "jest --coverage",
"buildDev": "rm -rf dist && webpack --mode development --config webpack.server.config.js && webpack --mode development --config webpack.dev.config.js",
"buildProd": "rm -rf dist && webpack --mode production --config webpack.server.config.js && webpack --mode production --config webpack.prod.config.js",
"start": "node ./dist/server.js"
},
"jest": {
"moduleNameMapper": {
"\\.(css|less)$": "<rootDir>/__mocks__/styleMock.js",
"\\.(gif|ttf|eot|svg)$": "<rootDir>/__mocks__/fileMock.js"
}
},
const adder = (x, y) => x + y
export default adder
import adder from '../adder'describe('Adder', () => {
test('adds two numbers', () => {
expect(adder(5, 3)).toEqual(8)
})
})

Binyamin

Written by

Binyamin

Software Engineering, Cloud Computing, and some really weird ideas… ;)

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade