Tutorials

MEAN App with Angular 2 and Webpack

Key Takeaways

  • Build Hello Alice by connecting TypeScript compilation, Webpack bundling, Angular bootstrap, and Express static-file serving.
  • Use a supported Angular release with compatible dependencies, explicit npm scripts, and a committed lockfile.
  • This example has no MongoDB or API layer, so it is not a complete MEAN application.
  • Treat the manual configuration as a learning exercise. Start most new Angular applications with Angular CLI.

An Angular Webpack setup connects a TypeScript entry point to a browser bundle, loads that bundle from HTML, and bootstraps an Angular component. You'll wire those pieces yourself, then use Express to serve a page displaying Hello Alice.

Angular CLI is the better productivity choice for most projects; manual configuration makes the boundaries easier to understand. If you want the standard workflow instead, start with our guide to installing Angular CLI.

What This Angular Webpack Setup Builds

The browser runs Angular. Node.js runs the build tools and Express. Express sends files; it does not render the Angular component in this example.

src/main.ts + app.component.ts
              |
       ts-loader / TypeScript
              |
           Webpack
              |
    dist/bundle.js + index.html
              |
     Express serves only dist/
              |
     Browser bootstraps Angular
              |
          Hello Alice

This deliberately small pipeline uses just-in-time compilation, or JIT: Angular compiles component templates in the browser. TypeScript compilation and Angular template compilation are different operations. The standard CLI workflow supports ahead-of-time compilation, or AOT, which compiles templates during the build; new CLI applications enable AOT by default.

Installing Your Dependencies

For this October 17, 2026 revision, use Angular 21.2.x, a supported LTS line, with Node.js 24.22.0 LTS. This is an intentional supported baseline, not a claim that Angular 21 is the newest major release.

ToolSelected baselineCompatibility consideration
Angular packages21.2.xKeep the installed Angular packages on the same patch.
Node.js24.22.0 LTSWithin Angular 21's supported ^24.0.0 range.
TypeScript5.9.xAngular 21 requires >=5.9.0 and <6.0.0.
RxJS7.8.xWithin Angular 21's supported ^7.4.0 range.
Zone.jsNot installedAngular 21 defaults to zoneless change detection.
Webpack / webpack-cli5.x / 7.xwebpack-cli 7 requires Node.js 20.9.0 or later.
ts-loader9.xIntegrates TypeScript with Webpack 5.

Check Angular's compatibility table before changing these release lines. Angular compatibility does not mean every listed Node.js branch is still maintained; use a maintained runtime as well.

Install Node.js 24.22.0, then create your project directory:

mkdir angular-webpack-lab
cd angular-webpack-lab
node --version
npm --version

Create this initial package.json. The installation commands below will add exact dependency versions to it.

{
  "name": "angular-webpack-lab",
  "version": "1.0.0",
  "private": true,
  "type": "module",
  "engines": {
    "node": "24.22.x"
  },
  "scripts": {
    "check": "tsc --noEmit",
    "build": "webpack --mode production",
    "build:dev": "webpack --mode development",
    "watch": "webpack --mode development --watch",
    "start": "node server.mjs"
  }
}

Install the browser dependencies, Express, and build tools:

npm install --save-exact @angular/common@21.2 @angular/compiler@21.2 @angular/core@21.2 @angular/platform-browser@21.2 rxjs@7.8 tslib@2 express@5
npm install --save-dev --save-exact typescript@5.9 webpack@5 webpack-cli@7 ts-loader@9 html-webpack-plugin@5
npm ls --depth=0

The release-line selectors resolve packages at installation time; --save-exact writes the resolved versions without version ranges. Commit the resulting package.json and package-lock.json, then use npm ci for subsequent clean installs. This freezes your resolved dependency tree rather than pretending these initial commands select identical patches forever.

Record 24.22.0 in a root-level .node-version file and select that runtime in your version manager and CI configuration. Add node_modules/ and dist/ to .gitignore.

Angular Packages

  • @angular/core provides the component decorator, signals, and core framework APIs.
  • @angular/platform-browser provides browser bootstrapping through bootstrapApplication().
  • @angular/common supplies common framework functionality and is a peer dependency of the browser platform package.
  • @angular/compiler supplies the runtime template compiler used by this JIT example.

These are npm packages, not NgModules. An NgModule organizes declarations and dependency-injection providers; it is not the framework's entire rendering system. Angular recommends standalone components and bootstrapApplication() for new code, so this example does not need app.module.ts.

RxJS

RxJS provides observables for working with asynchronous streams and satisfies Angular's RxJS peer dependency. Hello Alice does not need to create an observable.

Zone.js and core-js

Zone.js traditionally patches browser APIs so Angular can use asynchronous activity as a change-detection signal. Angular 21 is zoneless by default, so this project neither installs nor imports it. Updating a signal read by a template is one way to notify zoneless Angular about a state change.

No blanket core-js import is included. Choose polyfills for your actual browser and feature requirements; merely installing a package does not execute it in the browser.

Webpack and webpack-cli

Webpack follows imports and bundles the browser application. The separately installed webpack-cli provides the command used by the npm scripts. For more background, see our Webpack fundamentals tutorial.

TypeScript and tslib

TypeScript checks types and emits JavaScript. tslib supplies reusable runtime helpers when TypeScript's importHelpers option is enabled.

ts-loader

ts-loader invokes TypeScript compilation inside Webpack. It does not turn this configuration into an Angular AOT build.

html-webpack-plugin

This plugin generates the output HTML from your template and inserts the bundle's script tag. That keeps HTML generation inside the build instead of requiring a separate copy step.

Express

Express runs on Node.js and serves the generated frontend files. It has no database responsibilities here. If server middleware is unfamiliar, read our Node.js and Express tutorial.

Configuring TypeScript

Create the following files and directories. Only dist/ is generated.

angular-webpack-lab/
  .gitignore
  .node-version
  package.json
  package-lock.json
  tsconfig.json
  webpack.config.mjs
  server.mjs
  src/
    index.html
    main.ts
    app/
      app.component.ts
  dist/
    index.html
    bundle.js

Add tsconfig.json at the project root:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "Bundler",
    "lib": ["ES2022", "DOM"],
    "strict": true,
    "experimentalDecorators": true,
    "importHelpers": true,
    "sourceMap": true,
    "types": []
  },
  "include": ["src/**/*.ts"]
}

module: ESNext leaves module syntax for Webpack to process. moduleResolution: Bundler models bundler resolution, including extensionless relative imports. The decorator setting enables TypeScript's legacy decorator implementation; this component has no constructor dependencies, so it does not need emitted decorator type metadata.

Configuring Webpack

Create webpack.config.mjs:

import { fileURLToPath } from 'node:url';
import webpack from 'webpack';
import HtmlWebpackPlugin from 'html-webpack-plugin';

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

  return {
    mode: production ? 'production' : 'development',
    target: ['web', 'es2022'],
    entry: './src/main.ts',
    output: {
      path: fileURLToPath(new URL('./dist/', import.meta.url)),
      filename: 'bundle.js',
      publicPath: '/',
      clean: true
    },
    resolve: {
      extensions: ['.ts', '.js']
    },
    module: {
      rules: [
        {
          test: /\.ts$/,
          use: 'ts-loader',
          exclude: /node_modules/
        }
      ]
    },
    devtool: production ? false : 'source-map',
    plugins: [
      new webpack.DefinePlugin({
        __PRODUCTION__: JSON.stringify(production)
      }),
      new HtmlWebpackPlugin({
        template: './src/index.html',
        scriptLoading: 'defer'
      })
    ]
  };
};

The entry starts the dependency graph; the loader compiles your TypeScript; Webpack writes the result into dist/. Production mode enables Webpack's production optimizations, while this configuration disables production source maps. DefinePlugin replaces __PRODUCTION__ with a build-time boolean.

Create the HTML Entry Point

Create src/index.html. Do not add a script tag yourself; the HTML plugin inserts it into dist/index.html.

<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <base href="/">
    <title>Angular Webpack Lab</title>
  </head>
  <body>
    <my-app>Loading...</my-app>
  </body>
</html>

The generated bundle is requested as /bundle.js, not /dist/bundle.js: Express will mount the contents of dist/ at the site root.

Getting Started With Angular

Create the Hello Alice Component

Create src/app/app.component.ts:

import { Component, signal } from '@angular/core';

@Component({
  selector: 'my-app',
  standalone: true,
  template: '<h1>Hello {{ name() }}</h1>'
})
export class AppComponent {
  readonly name = signal('Alice');
}

The selector matches the host element in your HTML. The template reads the signal by calling name(); the initial rendered heading is Hello Alice.

Bootstrap the Application

Create src/main.ts:

import '@angular/compiler';
import { enableProdMode } from '@angular/core';
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app/app.component';

declare const __PRODUCTION__: boolean;

try {
  if (__PRODUCTION__) {
    enableProdMode();
  }
  await bootstrapApplication(AppComponent);
} catch (error) {
  console.error('Angular bootstrap failed:', error);
  document.body.textContent = 'Unable to start the application.';
}

The compiler import makes runtime template compilation available. bootstrapApplication() renders the standalone root component and returns a promise. The production branch disables Angular's development assertions; it does not perform AOT compilation.

Configuring Express to Serve Only Build Output

Create server.mjs at the project root:

import express from 'express';
import { existsSync } from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';

const publicDir = fileURLToPath(new URL('./dist/', import.meta.url));
const indexFile = path.join(publicDir, 'index.html');
const port = Number(process.env.PORT ?? 3000);
const host = process.env.HOST ?? '127.0.0.1';

if (!Number.isInteger(port) || port < 1 || port > 65535) {
  throw new Error('PORT must be an integer between 1 and 65535');
}
if (!existsSync(indexFile)) {
  throw new Error('Missing dist/index.html. Run npm run build first.');
}

const app = express();
app.disable('x-powered-by');

app.get('/', (_req, res, next) => {
  res.sendFile(indexFile, (error) => {
    if (error) next(error);
  });
});

app.use(express.static(publicDir, {
  index: false,
  dotfiles: 'ignore'
}));

app.use((_req, res) => {
  res.status(404).send('Not found');
});

app.use((error, _req, res, next) => {
  console.error(error);
  if (res.headersSent) return next(error);

  const status = Number.isInteger(error.status)
    && error.status >= 400 && error.status <= 599
    ? error.status : 500;
  res.status(status).send(status >= 500
    ? 'Internal server error' : 'Request failed');
});

app.listen(port, host, (error) => {
  if (error) {
    console.error('Server startup failed:', error);
    process.exitCode = 1;
    return;
  }
  console.log(`Listening on ${host}:${port}`);
});

The absolute static directory is resolved relative to the server file, not the shell's working directory. Only build output is public; the repository root and node_modules/ are outside that directory. The root route explicitly sends the HTML entry point.

Express error middleware needs four parameters, including next. The final handler logs details server-side without returning a stack trace, and the listen callback handles startup failures such as an occupied port.

The default binding is local-only on port 3000. Configure PORT and HOST through the deployment environment; containers commonly need HOST=0.0.0.0. This example has no client-side router, so unknown paths intentionally return 404 rather than an HTML fallback.

Running the App

Build the Project and Start the Server

npm run check
npm run build:dev
npm start

Open http://127.0.0.1:3000. You should see Hello Alice. In the browser's Network panel, verify that the HTML and /bundle.js load successfully; requests for /package.json and /src/main.ts should return 404.

Rebuild During Development

Keep Express running and open a second terminal in the project directory:

npm run watch

Webpack watches the imported source files and rebuilds when they change. Change Alice to Bob, wait for a successful build, and refresh the browser. This workflow has no automatic browser reload or hot-module replacement; restart Express after changing server.mjs.

Common Mistakes

  • Mixing incompatible releases: check the Angular compatibility matrix and inspect npm ls --depth=0 instead of bypassing dependency conflicts.
  • Removing the compiler import: this JIT example needs the runtime compiler; a plain TypeScript loader is not an Angular compiler replacement.
  • Editing generated HTML: edit src/index.html, because the next build regenerates the output.
  • Serving the repository root: public assets belong in a dedicated output directory, not beside package metadata and source code.

Before Using This Beyond a Learning Exercise

  • Move to the CLI's managed application build pipeline for AOT, template diagnostics, testing, and the development-server workflow. Modern CLI builds do not require maintaining this hand-written Webpack configuration.
  • Build before deployment. For this example, deploy dist/, server.mjs, package.json, and the lockfile; install server dependencies with npm ci --omit=dev. The build stage still needs development dependencies.
  • Review HTTPS, security headers, shutdown handling, and caching. Do not give the fixed bundle.js filename an immutable long-lived cache policy across changing releases.
  • Do not weaken Content Security Policy just to accommodate this teaching build. Angular's security guidance recommends AOT for production and discusses the additional policy requirements of JIT.

The useful lesson is the division of responsibility: TypeScript emits JavaScript, Webpack assembles browser assets, Angular bootstraps the UI, and Express delivers the files. Learn that boundary here; let higher-level tooling manage the repetitive configuration in your next application.

FAQ

Not for most new applications. Start with Angular CLI unless you have a concrete integration requirement or are deliberately studying the build pipeline; a custom configuration makes you responsible for compilation, assets, development tooling, and upgrades.

Does this example include MongoDB and qualify as a MEAN stack app?

No. It includes Angular, Express, and Node.js, but no MongoDB connection, API, or persistence, so it is not a complete MEAN application.

Which Node.js and TypeScript versions work with this example?

This example uses Node.js 24.22.0 and TypeScript 5.9.x with Angular 21.2.x. Angular 21 requires TypeScript greater than or equal to 5.9.0 and less than 6.0.0; check Angular's compatibility table before changing release lines.

How do I serve an Angular production build with Express?

Build the frontend first and configure Express to serve only its generated output directory, with an explicit route for the HTML entry point. This tutorial demonstrates that serving boundary, but its Webpack production build still uses JIT; use Angular's AOT pipeline for a production application.

What is the difference between TypeScript compilation and Angular compilation?

TypeScript compilation checks types and emits JavaScript; ts-loader invokes it during this Webpack build. Angular compilation turns component templates into rendering instructions, which this example generates in the browser using JIT.

Further Reading

Written by Sam Erickson

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