Tutorials

4 Easy Steps to Using Webpack with Your Node.js App

Key Takeaways

  • This setup bundles browser-facing JavaScript for a Node.js app; it does not bundle the Node.js server.
  • Webpack follows a dependency graph. Other asset types need the appropriate built-in handling or configured tooling.
  • Babel is optional. Its syntax transforms depend on your browser targets, and runtime polyfills are a separate concern.
  • The example maps dist/bundle.js to /assets/bundle.js in both development and Express.
  • Use a production build for deployment, watch mode for rebuilding files on disk, and Dev Server for serving and refreshing the frontend.

To use Webpack with a Node.js app, give Webpack a browser-side entry point, configure its output, reference that output from HTML, and serve the files. This four-step Webpack Node.js app setup gives you a working frontend build, an Express server, and an optional Babel configuration.

The important boundary is where the code runs: Node.js runs the build tools and your server; the browser runs the generated bundle.

What Webpack Bundles in This Node.js App

Webpack starts at an entry point and follows imports to construct a dependency graph: the collection of modules and assets your application depends on. That graph can include more than JavaScript, but this tutorial deliberately configures only a JavaScript build. It does not set up Sass/LESS compilation, image optimization, or a font pipeline.

src/index.js + imported modules
              |
       Webpack + optional Babel
              |
        dist/bundle.js
              |
   Express serves /assets/bundle.js
              |
           Browser

server.js runs directly in Node.js, outside this build.

If you’re comparing bundlers rather than implementing this setup, start with our guide to JavaScript build tools.

1) Create the Project and Install Packages

Use an up-to-date Node.js 24 LTS installation, at least 24.11.0. This tutorial chooses that runtime so the optional Babel 8 configuration works too; check the Node.js release schedule when selecting a runtime for an existing project. Our guide to which Node.js version to use explains the release channels.

Create a directory:

mkdir webpack-node-demo
cd webpack-node-demo

Create package.json with these contents:

{
  "name": "webpack-node-demo",
  "private": true,
  "type": "module",
  "engines": {
    "node": "^24.11.0"
  },
  "scripts": {
    "build": "webpack --mode production",
    "watch": "webpack --mode development --watch",
    "dev": "webpack serve --mode development",
    "start": "node server.js"
  }
}

The type field makes the project’s .js files use ECMAScript modules, so the server and configuration use import and export.

Install Express as a runtime dependency and the build tools as development dependencies:

npm install express@5
npm install --save-dev "webpack@^5.101.0" webpack-cli@7 webpack-dev-server@5
npm ls --depth=0

Webpack CLI 7 requires Node.js 20.9.0 or newer and Webpack 5.101.0 or newer; its Dev Server integration requires webpack-dev-server 5 or newer. The CLI compatibility requirements are the reference when upgrading these packages together.

These commands select release ranges, not permanently fixed versions. The final command lists the installed top-level packages. Commit package.json and package-lock.json; use npm ci for subsequent clean, lockfile-based installs.

Create the remaining files as you work through the next steps:

webpack-node-demo/
  .browserslistrc
  package.json
  package-lock.json
  webpack.config.js
  server.js
  public/
    index.html
  src/
    greet.js
    index.js
  dist/                 generated by the build

2) Configure Webpack and the Browser Entry Point

Create .browserslistrc:

defaults

This explicitly selects Browserslist’s default browser set. Review that policy against your users’ requirements; it is not a promise of ES5 output. Webpack’s browserslist target uses the policy to choose features for its generated runtime, while Babel can use the same policy to transform your application code.

Create webpack.config.js:

import { fileURLToPath } from "node:url";

export default (_env, argv) => {
  const production = argv.mode === "production";

  return {
    mode: production ? "production" : "development",
    target: "browserslist",
    entry: "./src/index.js",
    output: {
      path: fileURLToPath(new URL("./dist/", import.meta.url)),
      filename: "bundle.js",
      publicPath: "/assets/",
      clean: true,
    },
    devtool: production ? false : "source-map",
    module: {
      rules: [],
    },
    devServer: {
      host: "127.0.0.1",
      port: 8080,
      static: {
        directory: fileURLToPath(new URL("./public/", import.meta.url)),
        publicPath: "/",
      },
      hot: false,
      liveReload: true,
    },
  };
};

The essential configuration is still small: entry identifies the starting module, and output names the generated file and its location. output.path is a filesystem directory; output.publicPath is the browser-facing asset base URL. Neither setting creates an Express route.

This configuration enables separate source maps during development and emits no source maps in production. Source maps let browser debugging tools map bundled code back to your source files.

Create src/greet.js:

export function greet(name) {
  return `Hello, ${name}! Your browser bundle is working.`;
}

Create src/index.js:

import { greet } from "./greet.js";

const message = document.querySelector("#message");

if (!message) {
  throw new Error("Missing #message element");
}

message.textContent = greet("Node.js");

Optional: Add Babel for Target-Dependent Syntax Transforms

You don’t need Babel merely to bundle modules. Add it when your source uses syntax that your supported browsers cannot run, or when your project needs a particular Babel transform.

Install Babel 8 and a compatible loader:

npm install --save-dev @babel/core@8 @babel/preset-env@8 "babel-loader@^10.1.0"

babel-loader 10.1.0 added Babel 8 support. Babel 8’s runtime requirements include Node.js 24.11.0 or newer, which is why the project declares that minimum.

Create babel.config.json:

{
  "presets": ["@babel/preset-env"]
}

Replace the empty module.rules array in webpack.config.js with this fragment:

// Fragment: replacement value for module.rules.
[
  {
    test: /\.m?js$/,
    exclude: /node_modules/,
    use: "babel-loader",
  },
]

@babel/preset-env reads the configured browser targets and selects syntax transforms accordingly. The earlier “ES6 to ES5” description was too broad: this setup does not guarantee ES5, and the rule deliberately excludes dependencies in node_modules.

A polyfill supplies a runtime feature that an environment lacks; changing syntax is a different job. This Babel configuration adds no runtime polyfills. Babel 8 removed @babel/preset-env’s useBuiltIns option; its migration guide directs projects needing automatic core-js polyfill injection to babel-plugin-polyfill-corejs3. Browser APIs need their own compatibility assessment too.

3) Add HTML and Serve the Bundle from Node.js

Create public/index.html. The script URL matches the asset URL configured above, not the directory name on disk:

<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Webpack with Node.js</title>
    <script defer src="/assets/bundle.js"></script>
  </head>
  <body>
    <h1>Webpack with Node.js</h1>
    <p id="message">Loading the browser bundle…</p>
  </body>
</html>

Create server.js:

import express from "express";
import { fileURLToPath } from "node:url";

const app = express();
const distDirectory = fileURLToPath(new URL("./dist/", import.meta.url));
const publicDirectory = fileURLToPath(new URL("./public/", import.meta.url));
const port = Number(process.env.PORT ?? 3000);

app.use("/assets", express.static(distDirectory));
app.use(express.static(publicDirectory));

app.listen(port, (error) => {
  if (error) {
    console.error("Server failed to start:", error);
    process.exitCode = 1;
    return;
  }

  console.log(`Open http://127.0.0.1:${port}/`);
});

Express static middleware exposes dist under /assets and serves the page from public. Therefore, a request for /assets/bundle.js reads dist/bundle.js; the URL does not contain dist. The startup callback follows Express 5’s listen-error handling contract.

Webpack never receives server.js as an entry point. Node.js executes it directly. For more server-side context, see our Node.js and Express tutorial.

4) Build, Run, and Choose a Development Workflow

Build and Run the Node.js App

npm run build
npm start

Open http://127.0.0.1:3000/. You should see “Hello, Node.js! Your browser bundle is working.” The build writes dist/bundle.js, and Express serves that file alongside the authored HTML.

The build script explicitly selects production mode, which enables JavaScript minimization and other production-oriented optimizations. It does not start a server or make Babel add missing runtime APIs.

Use Watch Mode When Express Must Serve the Frontend

Run this in one terminal and npm start in another:

npm run watch

Watch mode rebuilds the bundle on disk when its source dependencies change. Express can serve the updated file, but you must refresh the page yourself. This command does not restart the Node.js server.

Use Webpack Dev Server for the Standalone Frontend

npm run dev

Open http://127.0.0.1:8080/. The Dev Server configuration serves the HTML from public and the compiled bundle under /assets/. It explicitly disables hot module replacement and enables full-page live reload, so this plain JavaScript example needs no application-side HMR integration.

Dev Server serves compiled assets from memory by default; it is not a replacement for the production build. It also does not run server.js. If your frontend needs backend endpoints, run the backend separately and configure a development proxy.

CommandWrites the bundle to disk?Serves the frontend?Browser updates
npm run buildYes, onceNoNone
npm run watchYes, after changesNo; use Express separatelyManual refresh
npm run devNo, by defaultYes, on port 8080Full-page reload in this setup

Webpack’s development guide compares these workflows. HMR updates modules without a full page reload, but preserving application state requires compatible module handling or a framework integration; simply enabling it is not enough.

Common Mistakes and Deployment Checks

  • Confusing browser and server targets. Without a Browserslist configuration, Webpack defaults to a web target. Bundling a Node.js server is a separate configuration involving a Node target, built-ins, and dependency externalization.
  • Opening the HTML as a local file. Use one of the HTTP servers above so the root-relative /assets/bundle.js URL resolves correctly.
  • Deploying an in-memory development build. Run npm ci and npm run build in your build environment, then deploy dist, public, and the server’s runtime requirements.
  • Caching an unversioned filename indefinitely. This tutorial keeps bundle.js for a transparent HTML example. Before enabling long-lived asset caching, adopt content-hashed filenames and update the HTML references as part of the build.
  • Treating source maps as access control. If you enable production maps for error reporting, keep them out of publicly served directories unless publishing them is intentional.
  • Exposing development tooling publicly. Keep Dev Server local, and review deployment security separately from whether the bundle builds successfully.

FAQ

Does Webpack bundle a Node.js server or browser assets by default?

Without a Browserslist configuration, Webpack defaults to a web target. This tutorial explicitly selects browser targets through Browserslist and bundles only frontend code; server.js runs directly in Node.js.

How do I serve Webpack’s dist directory from a Node.js app?

In Express, mount the absolute dist directory with express.static at your chosen URL prefix. This example mounts it at /assets, so dist/bundle.js is available at /assets/bundle.js.

Do I still need Babel when using Webpack?

No. Use Babel when your application syntax needs transformation for your supported browsers or when you need specific Babel transforms. The browser targets determine the transformations, and this tutorial’s Babel configuration does not add runtime polyfills.

What is the difference between webpack --watch and webpack serve?

webpack --watch rebuilds files on disk but does not serve them or refresh the browser. webpack serve runs Webpack Dev Server, which serves compiled assets from memory by default and supports live reload or HMR depending on configuration.

Can I bundle CSS with Webpack too?

Yes, but this tutorial demonstrates a JavaScript-only build. Webpack’s native CSS guide explains its built-in CSS pipeline, configuration, and experimental status. Sass/LESS compilation and image optimization require their own tooling.

Further Reading

Written by Sam Erickson

Software engineer. I write every article on StackChief myself. Author of Mastering the Tech Interview. More about me