Angular Multi-Environment Builds

Mirza Leka
12 min readNov 20, 2024

--

This article is an in-depth guide to Angular build configuration. You’ll learn to serve, build, and distribute your project for different environments and deployment use cases.

What will we learn:

  • Running Angular builds
  • Serving dist static files in the browser
  • Angular.json build configuration
  • Angular environment files

Then, we’ll apply that knowledge by:

  • Creating custom multi-environment configurations
  • Separate dist per environment

Peek into Angular builds

For this guide, I created a new project: angular-multi-env app using Angular v18 and Node.js v20. The complete source code is in the repository linked at the bottom.

This is what the package.json file looks like:

{
"name": "angular-multi-env",
"version": "0.0.0",
"scripts": {
"ng": "ng",
"start": "ng serve",
"build": "ng build",
"watch": "ng build --watch --configuration development",
"test": "ng test"
},
}

Let’s do a quick build and see what happens.
We can do this in several ways:

> npm run build // or
> ng build // or
> ng b

Upon executing the build, Angular will generate a dist folder in the root directory.

> ng build

Initial chunk files | Names | Raw size | Estimated transfer size
main-O74TZ7OY.js | main | 206.72 kB | 55.67 kB
polyfills-FFHMD2TL.js | polyfills | 34.52 kB | 11.28 kB
styles-5INURTSO.css | styles | 0 bytes | 0 bytes

| Initial total | 241.24 kB | 66.95 kB

Application bundle generation complete. [4.875 seconds]

Output location: .../angular-multi-env/dist/angular-multi-env

If we go to the specified location, you’ll find the browser directory containing all the distributable files.

These files represent the compiled (and compressed) code version that can run in the browser. However, we will need to do more than double-clicking on the index.html file. We need an HTTP server to serve these files.

Serving static files

We need to set up a backend server to serve Angular dist files in the browser. The backend API can be written in any language you desire, such as Express.js, Nginx, etc. The simplest way to set one is to install the
http-server npm package that runs a server in the CLI.

> npm i -g http-server
// once installed globally (-g), it can be used anywhere on the machine

Now, let’s use it to serve the files in the browser directory:

dist/angular-multi-env/browser>http-server

Starting up http-server, serving ./

http-server version: 14.1.1

http-server settings:
CORS: disabled
Cache: 3600 seconds
Connection Timeout: 120 seconds
Directory Listings: visible
AutoIndex: visible
Serve GZIP Files: false
Serve Brotli Files: false
Default File Extension: none

Available on:
http://172.21.96.1:8081
http://192.168.0.14:8081
http://127.0.0.1:8081
Hit CTRL-C to stop the server

If you go to one of the specified IPs, the site should open as expected:

Understanding the Angular.json build configuration

By default, the Angular builds (and packages) files in one of two ways:

  • Production
    Slower build speed, optimized for performance and smaller bundle size. It uses AOT compilation, three-shaking, code minification, and disables source maps.
  • Development
    Faster iteration by rebuilding the app in the browser on file changes. It includes source maps, hot module replacement, and debugging capabilities but lacks production optimizations.

Configuring builds

To find these builds, open up the angular.json file in the root directory and scroll down to the configurations section.

  • projects /<your-project-name> / architect / build / configurations
{
"projects": {
"angular-multi-env": {
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
"outputPath": "dist/angular-multi-env",
"index": "src/index.html",
"browser": "src/main.ts"
},
"configurations": {
"production": { // <-- production build
"budgets": [
{
"type": "initial",
"maximumWarning": "500kB",
"maximumError": "1MB"
},
{
"type": "anyComponentStyle",
"maximumWarning": "2kB",
"maximumError": "4kB"
}
],
"outputHashing": "all"
},
"development": { // <-- development build
"optimization": false,
"extractLicenses": false,
"sourceMap": true
}
},
"defaultConfiguration": "production"
}
}
}
}
}

When we ran the build command (> ng build), we created the application production build. How can we be certain?
First, if you open one of the files, you’ll notice that the code is so minified that it looks like snow.

The second reason is the default build configuration in the angular.json file. If we look up the build section

  • projects /<your-project-name> / architect / build,

we’ll find the key that says: “defaultConfiguration”: “production”:

          "configurations": {
"production": {
"budgets": [
{
"type": "initial",
"maximumWarning": "500kB",
"maximumError": "1MB"
},
{
"type": "anyComponentStyle",
"maximumWarning": "2kB",
"maximumError": "4kB"
}
],
"outputHashing": "all"
},
"development": {
"optimization": false,
"extractLicenses": false,
"sourceMap": true
}
},
"defaultConfiguration": "production" // <-- here it is
},

To run the development build, we can either change the “defaultConfiguration” form “production” to “development” or run the build with the — configuration developmentflag.

> ng build --configuration development

This will output more files, and the code inside will also differ.

Ng Serve configuration

Below the build configuration section, there is a serve section that describes how the app runs in the serve mode:

  "projects": {
"angular-multi-env": {
"projectType": "application",
"root": "",
"prefix": "app",
"architect": {
...
"serve": { // <-- here it is
"builder": "@angular-devkit/build-angular:dev-server",
"configurations": {
"production": {
"buildTarget": "angular-multi-env:build:production"
},
"development": {
"buildTarget": "angular-multi-env:build:development"
}
},
"defaultConfiguration": "development"
}
}
}
}

It also defaults to the development mode but can be changed to any other configuration option.

Distribution directory

The dist ( “distribution”) directory stores the application's compiled and optimized output after the build. The files in the dist folder are often versioned or hashed to facilitate cache busting (using outputHashing), ensuring that users receive the latest version of your application after updates.

The location path of the dist folder is also set in the angular.json file under the outputPath property.

  • projects /<your-project-name> / architect / build / options
        "build": {
"builder": "@angular-devkit/build-angular:application",
"options": {
"outputPath": "dist/angular-multi-env", // <-- here it is
"index": "src/index.html",
"browser": "src/main.ts",
...
},

This can be modified to any location you desire.
In addition, you can have multiple dist folders, one for each environment in the configuration. We’ll learn more about that later.

Creating Environment files

While some frameworks use .env files to store environment variables and API keys, Angular has a built-in file called environment.ts. Before version v15, the environment file was created upon project creation. In modern versions, they need to be generated manually:

> ng g environments

This command generates an environments directory in src directory, and two files inside:

  • environment.ts
  • environment.development.ts

It also updates the angular.json file, the development section with a new fileReplacements property :

            "development": {
...,
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.development.ts"
}
]
}

We should only use the environment.ts file in the code, as it is the main env file for both the production and development environment.

The fileReplacements property tells Angular that whenever we’re using the environment.ts file in the given configuration mode (in this case, “development”), the contents of environment.ts file will be overridden by those in the environment.development.ts file.

Overriding environment files in action

Here is the default (production) environment file:

// environment.ts
export const environment = {
production: true,
apiBaseURL: 'https://myapp.com/api',
greeting: 'Hello PROD!'
};

Also, the development environment file:

// environment.development.ts
export const environment = {
production: false,
apiBaseURL: 'http://localhost:3000/api',
greeting: 'Hello DEV!'
};

Two share the same keys but have different values. Now, import that environment file in the components and services, such as:

import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';
import { environment } from '../environments/environment';

@Component({
selector: 'app-root',
standalone: true,
imports: [RouterOutlet],
templateUrl: './app.component.html',
styleUrl: './app.component.scss'
})
export class AppComponent {
title = environment.greeting // <-- using the env file
}

Notice that we’re only using the default environment file.
If we run the DEV build, Angular will override the values from the main environment file with those from the environment development file.

Otherwise, in the production mode, the app will use the default environment.ts file specified in the angular.json configuration.

Using the previously learned techniques, we’ll create a multi-env build configuration. Start by creating two additional env files (4 in total):

  • environment.ts
  • environment.production.ts
  • environment.staging.ts
  • environment.development.ts

DEV Configuration

We can make use of the environment file we created earlier:

// environment.development.ts
export const environment = {
production: false,
apiBaseURL: 'http://localhost:3000/api',
greeting: 'Hello DEV!'
};

Likewise, the angular.json configuration can also be updated, with optimization disabled, source maps enabled, and file replacements set:

          "configurations": {
"development": {
"optimization": false,
"extractLicenses": false,
"sourceMap": true,
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.development.ts"
}
]
}
},

Configuration rules:

  • Optimization: false
    Indicates that this build will not be minified for performance purposes. This is something to use in production.
  • ExtractLicenses: false
    Extract Licenses help with compliance by making license information easily accessible. This is not needed for DEV mode.
  • SourceMap: true
    The source maps allow the source code to be displayed in the browser in development mode. This makes it easier to debug the application, but it should be avoided in production for security reasons.
  • FileReplacements
    As discussed before, we use this setting to replace the default environment.ts file with the one specified.

We’ll also keep the serve section in the angular.json file as is:

        "serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"configurations": {
"development": {
"buildTarget": "angular-multi-env:build:development"
}
},
"defaultConfiguration": "development"
},

STAGING Configuration

Start with updating the (previously created) environment.staging.ts with some dummy values:

// environment.staging.ts
export const environment = {
production: true,
apiBaseURL: 'https://myapp.staging.com/api',
greeting: 'Hello STAGING!'
};

Then we add the “staging” section in angular.json builder configurations:

          "configurations": {
"staging": {
"aot": true,
"optimization": true,
"sourceMap": false,
"extractLicenses": true,
"outputHashing": "all",
"budgets": [
{
"type": "initial",
"maximumWarning": "2MB",
"maximumError": "4MB"
},
{
"type": "anyComponentStyle",
"maximumWarning": "4kB",
"maximumError": "8kB"
}
],
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.staging.ts"
}
]
}
},

We’ve created a hybrid of development and production configurations.

Configuration rules:

  • AOT: true
    Enables Ahead-of-Time (AOT) compilation, which pre-compiles Angular templates and components during the build process rather than at runtime.
  • Optimization: true
    Indicates that this build will have tree-shaking and be minified for performance purposes.
  • SourceMap: false
    The source code won’t be exposed in the browser.
  • ExtractLicenses: true
    Extracts third-party licenses into a separate file.
  • Outputhashing: all
    Appends a unique hash to filenames of output for JavaScript and CSS files, e.g., default: main.js, with hashing: main-Y6OULDNJ.js.
    This ensures browser cache invalidation for updated files while keeping unchanged files cached.
  • Budgets
    Sets the build size to warn or fail if certain size thresholds are exceeded. I increased the budget limits for staging from KB to MB to make build warnings less strict.
  • FileReplacements
    Once again, the fileReplacements property specifies that the default environment file will be replaced with a newly created environment.staging.ts file.

We’ll also add the staging configuration to the server builder section:

        "serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"configurations": {
"staging": {
"buildTarget": "angular-multi-env:build:staging"
},
"development": {
...
}
},
"defaultConfiguration": "development"
},

PRODUCTION Configuration

The newly created environment.production.ts will have the same configuration as the default environment.ts. Two coexist because having the production config in the production env file is much more convenient.

// environment.production.ts
export const environment = {
production: true,
apiBaseURL: 'https://myapp.com/api',
greeting: 'Hello PROD!'
};

Now, onto updating the production configuration file:

          "configurations": {
"production": {
"outputHashing": "all",
"budgets": [
{
"type": "initial",
"maximumWarning": "500kB",
"maximumError": "1MB"
},
{
"type": "anyComponentStyle",
"maximumWarning": "2kB",
"maximumError": "4kB"
}
],
"fileReplacements": [ // <-- here it is
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.production.ts"
}
],
}
}

This config excludes most of the rules set in the staging configuration build because AOT, Optimization, ExtractLicenses, and Tree-Shaking are all enabled by default in the production build. The source maps are disabled in production by default.

As with previous cases, we’ll add the fileReplacements property that overrides the contents of the default environment file with the production env file.

The serve configuration can stay as it is:

        "serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"configurations": {
"production": {
"buildTarget": "angular-multi-env:build:production"
},
"staging": {
"buildTarget": "angular-multi-env:build:staging"
},
"development": {
"buildTarget": "angular-multi-env:build:development"
}
},

Ng Serve per Environment

The ng serve command is used to build and serve the application in the browser, rebuilding on file changes. It runs like this:

> ng serve

The default to serve is in the development mode, as specified in the angular.json file:

"projects": {
"angular-multi-env": {
...,
"architect": {
...
"serve": {
"configurations": {
"production": {...},
"development": {...}
},
"defaultConfiguration": "development" // <-- here it is
}
}
}
}

Since we separated the application into multiple environments, we can now serve content per environment as well:

// DEV (uses environment.development.ts)
> ng serve --configuration development

// STAGING (uses environment.staging.ts)
> ng serve --configuration staging

// PRODUCTION (uses environment.production.ts)
> ng serve --configuration production

We now have the power of hot reloading with ng serve and use environment-specific variables.

Additional build scripts

Now that we are done with environment configurations, we’ll add new NPM scripts in the package.json file that will make it easier to build the application for each environment:

// package.json file
"scripts": {
"ng": "ng",
"start": "ng serve",

// new build scripts:
"build:development": "ng build --configuration development",
"build:staging": "ng build --configuration staging",
"build:production": "ng build --configuration production",

"watch": "ng build --watch --configuration development",
"test": "ng test"
},

With this in place, we can easily generate a build for each environment:

// DEV
> npm run build:development

// STAGING
> npm run build:staging

// PRODUCTION
> npm run build:production

Using NPM Hooks with Builds

Furthermore, we can add pre- and post-scripts to perform actions before and after the build. For instance, we can create a script that starts an HTTP server that runs the static files from the dist folder and tell it to run after every production build.

"postbuild:production": "http-server dist/angular-multi-env/browser"
 // package.json
"scripts": {
"ng": "ng",
"start": "ng serve",
"build:development": "ng build --configuration development",
"build:staging": "ng build --configuration staging",
"build:production": "ng build --configuration production",

// automatically trigerred AFTER build:production
"postbuild:production": "http-server dist/angular-multi-env/browser",

"watch": "ng build --watch --configuration development",
"test": "ng test"
},
> npm run build:production

// PROD BUILD

> angular-multi-env@0.0.0 build:production
> ng build --configuration production

Initial chunk files | Names | Raw size | Estimated transfer size
main-Y6OULDNJ.js | main | 206.79 kB | 55.73 kB
polyfills-FFHMD2TL.js | polyfills | 34.52 kB | 11.28 kB
styles-5INURTSO.css | styles | 0 bytes | 0 bytes

| Initial total | 241.31 kB | 67.01 kB

Application bundle generation complete. [4.709 seconds]

Output location: angular-multi-env/dist/angular-multi-env

// POST PROD-BUILD

> angular-multi-env@0.0.0 postbuild:production
> http-server dist/angular-multi-env/browser

Starting up http-server, serving dist/angular-multi-env/browser

http-server version: 14.1.1

...

Available on:
http://172.21.96.1:8081

Finally, we can separate distributable folders per environment for dev, staging and production.

Start by setting the default build to dist.dev:

      "architect": {
"build": {
"builder": "@angular-devkit/build-angular:application",
"options": {
"outputPath": "dist.dev/angular-multi-env", // <-- default
...
},
}
}

Then include the unique output path into each build environment build configuration:

        "build": {
"builder": "@angular-devkit/build-angular:application",
"options": {
"outputPath": "dist.dev/angular-multi-env",
...
},
"configurations": {
"production": { // prod build
"outputPath": "dist.prod/angular-multi-env",
...
},
"staging": { // staging build
"outputPath": "dist.staging/angular-multi-env",
...
},
"development": { // dev build
"outputPath": "dist.dev/angular-multi-env",
...
}
},
"defaultConfiguration": "production"
},

Rerunning the same three build scripts:

// DEV
> npm run build:development

// STAGING
> npm run build:staging

// PRODUCTION
> npm run build:production

Creates an environment-specific dist folder:

Make sure also to add these paths to the git ignore file:

// .gitignore

# Compiled output
/dist
/dist.*

--

--

Mirza Leka
Mirza Leka

Written by Mirza Leka

Web Developer. DevOps Enthusiast. I share my experience with the rest of the world. Follow me on https://twitter.com/mirzaleka for news & updates #FreePalestine

No responses yet