| |

Angular 109 🅰️ Environment Configuration

⏱️ Estimated read: 14 min | |

An Angular application runs in more than one place. It runs on a developer’s laptop, on a staging server, and in production. Each environment has different settings: a different API URL, a different feature flag state, a different logging level. The application code should not change between environments. Only the configuration should. Environment configuration is the mechanism that keeps the application code constant while the settings vary.

The previous chapters covered the application itself — components, services, state, testing, performance, debugging. This chapter covers the layer around the application: the environment files, the build configurations, the runtime configuration strategies, and the security considerations that determine what should and should not be stored in the environment files.

Key point: The goal of environment configuration is to make the application build once and run everywhere. The same compiled JavaScript should work in development, staging, and production. The differences are supplied at build time or at runtime. The build-time approach uses the environment files and the build configurations. The runtime approach loads the configuration from an external source after the application starts. The choice depends on whether the deployment pipeline rebuilds for each environment or promotes the same artifact.


Why Environment Configuration Matters

A hardcoded API URL is a bug waiting to happen. The application that points to http://localhost:3000 works on the developer’s machine. It fails in production. The developer who fixes the URL in the source code and rebuilds creates a new artifact. The artifact that was tested is not the artifact that runs. The environment configuration removes this problem.

The URL problem. The API URL differs between environments. Development uses a local mock. Staging uses a staging API. Production uses the production API. The URL is the same concept with a different value in each environment.

The feature flag problem. A new feature is under development. It should be enabled in staging but disabled in production. A feature flag in the environment file controls the behavior without changing the code.

The logging problem. Development wants verbose logging. Production wants only errors. The logging level is an environment setting.

The secret problem. API keys, database credentials, and other secrets differ between environments. They must not be stored in the source code or in the frontend bundle. Environment configuration must distinguish between public settings and secrets.

The trade-off. Environment configuration adds indirection. The application reads a value from a configuration object instead of a hardcoded constant. The indirection is the price of flexibility. Without it, every environment change requires a code change and a rebuild.


a. The Environment Files

The Angular CLI generates environment.ts and environment.development.ts in the src/environments/ directory. The files export a constant object with the environment-specific settings.

// src/environments/environment.ts
export const environment = {
  production: true,
  apiUrl: 'https://api.example.com',
  logLevel: 'error',
  featureFlags: {
    newDashboard: false,
  },
};
// src/environments/environment.development.ts
export const environment = {
  production: false,
  apiUrl: 'http://localhost:3000',
  logLevel: 'debug',
  featureFlags: {
    newDashboard: true,
  },
};

The application imports the environment object and reads the settings.

import { environment } from '../environments/environment';

@Injectable({ providedIn: 'root' })
export class ApiService {
  private http = inject(HttpClient);

  getUsers(): Observable<User[]> {
    return this.http.get<User[]>(`${environment.apiUrl}/users`);
  }
}

The TypeScript compiler resolves the import to the correct file based on the build configuration. The fileReplacements array in angular.json tells the CLI which file to use for each configuration.

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

The fileReplacements is the mechanism that makes the build-time environment switch work. The source code imports environment from environment.ts. The build replaces the file with the environment-specific file. The compiled bundle contains the values of the selected environment.

The ng build --configuration production command uses the production configuration. The ng serve command uses the development configuration by default.


b. Runtime Configuration

The build-time approach has a limitation. The environment values are baked into the bundle. To change them, the application must be rebuilt and redeployed. For a pipeline that promotes the same artifact through environments, this is a problem. The artifact that was tested in staging is not the artifact that runs in production if the values differ.

The runtime approach solves the problem. The application loads the configuration from an external source after it starts. The same artifact runs in every environment. The configuration is provided by the environment.

The most common pattern is an assets/config.json file that is fetched at application startup. The file is not part of the compiled bundle. It is served by the web server and can be replaced without rebuilding the application.

// src/app/config.service.ts
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { firstValueFrom } from 'rxjs';

export interface AppConfig {
  apiUrl: string;
  logLevel: string;
  featureFlags: Record<string, boolean>;
}

@Injectable({ providedIn: 'root' })
export class ConfigService {
  private http = inject(HttpClient);
  private config: AppConfig | null = null;

  async load(): Promise<void> {
    this.config = await firstValueFrom(
      this.http.get<AppConfig>('/assets/config.json')
    );
  }

  get apiUrl(): string {
    if (!this.config) throw new Error('Config not loaded');
    return this.config.apiUrl;
  }

  get logLevel(): string {
    if (!this.config) throw new Error('Config not loaded');
    return this.config.logLevel;
  }

  isFeatureEnabled(name: string): boolean {
    if (!this.config) throw new Error('Config not loaded');
    return this.config.featureFlags[name] ?? false;
  }
}

The service is loaded before the application bootstraps. The provideAppInitializer function (Angular 19+) or the APP_INITIALIZER token ensures that the configuration is loaded before the first component is rendered.

// app.config.ts
import { ApplicationConfig, provideAppInitializer, inject } from '@angular/core';
import { provideHttpClient } from '@angular/common/http';
import { ConfigService } from './config.service';

export const appConfig: ApplicationConfig = {
  providers: [
    provideHttpClient(),
    provideAppInitializer(() => inject(ConfigService).load()),
  ],
};

The provideAppInitializer function runs the loader before the application is stable. The first component renders only after the configuration is loaded. The application reads the configuration from the service.

// public/assets/config.json
{
  "apiUrl": "https://api.example.com",
  "logLevel": "error",
  "featureFlags": {
    "newDashboard": false
  }
}

The config.json file is in the public/assets/ directory. It is copied to the build output. The deployment pipeline replaces the file for each environment. The same bundle runs in every environment with the environment-specific configuration.

The runtime approach is the modern recommendation. It separates the build from the configuration and enables the “build once, deploy anywhere” principle. The build-time approach remains useful for smaller projects or for settings that are truly constant across environments.


c. Secrets and Security

Environment configuration is not the place for secrets. Any value in the frontend bundle is visible to the user. Any value in a config.json file served by the web server is visible to anyone who requests it.

The public key problem. API keys and other secrets must not be stored in the environment files or the config.json file. The frontend bundle is downloaded by the browser. The user can read every value in it. A secret in the bundle is a secret that has been published.

The server-side problem. Secrets belong on the server. The frontend calls the backend. The backend holds the secret. The backend uses the secret to call the third-party service. The frontend never sees the secret.

The environment-specific secret problem. If the frontend must hold a value that varies by environment, the value should be provided by the backend. The backend exposes an endpoint that returns the public configuration. The frontend calls the endpoint. The endpoint returns only values that are safe to expose.

The .gitignore problem. The environment files that contain secrets must be excluded from version control. The .gitignore file should list the sensitive files. The repository should contain a template file with placeholder values. The deployment pipeline injects the real values.

The environment.production.ts problem. The environment file for production is often committed to the repository with production values. If the values include secrets, the secrets are in the repository. The values should be non-sensitive. The secrets should be injected at build time by the pipeline.

The rule is simple: the frontend is a public place. Every value in the frontend is public. Use the frontend for public configuration. Use the backend for secrets. The backend is the only place where a secret is safe.


Complete Example Session

This session demonstrates both the build-time and the runtime configuration approaches.

// ============================================
// PART 1: THE BUILD-TIME ENVIRONMENT FILES
// ============================================

// src/environments/environment.ts
export const environment = {
  production: true,
  apiUrl: 'https://api.example.com',
  logLevel: 'error',
  featureFlags: {
    newDashboard: false,
  },
};

// src/environments/environment.development.ts
export const environment = {
  production: false,
  apiUrl: 'http://localhost:3000',
  logLevel: 'debug',
  featureFlags: {
    newDashboard: true,
  },
};

// ============================================
// PART 2: THE BUILD CONFIGURATION
// ============================================

// angular.json
{
  "projects": {
    "my-app": {
      "architect": {
        "build": {
          "configurations": {
            "production": {
              "fileReplacements": [
                {
                  "replace": "src/environments/environment.ts",
                  "with": "src/environments/environment.production.ts"
                }
              ]
            },
            "development": {
              "fileReplacements": [
                {
                  "replace": "src/environments/environment.ts",
                  "with": "src/environments/environment.development.ts"
                }
              ]
            }
          }
        }
      }
    }
  }
}

// ============================================
// PART 3: USING THE ENVIRONMENT
// ============================================

import { environment } from '../environments/environment';

@Injectable({ providedIn: 'root' })
export class ApiService {
  private http = inject(HttpClient);

  getUsers(): Observable<User[]> {
    return this.http.get<User[]>(`${environment.apiUrl}/users`);
  }
}

// ============================================
// PART 4: THE RUNTIME CONFIG
// ============================================

// public/assets/config.json
{
  "apiUrl": "https://api.example.com",
  "logLevel": "error",
  "featureFlags": {
    "newDashboard": false
  }
}

// ============================================
// PART 5: THE CONFIG SERVICE
// ============================================

@Injectable({ providedIn: 'root' })
export class ConfigService {
  private http = inject(HttpClient);
  private config = signal<AppConfig | null>(null);

  async load(): Promise<void> {
    const config = await firstValueFrom(
      this.http.get<AppConfig>('/assets/config.json')
    );
    this.config.set(config);
  }

  get apiUrl(): string {
    return this.config()?.apiUrl ?? '';
  }

  get logLevel(): string {
    return this.config()?.logLevel ?? 'error';
  }

  isFeatureEnabled(name: string): boolean {
    return this.config()?.featureFlags[name] ?? false;
  }
}

// ============================================
// PART 6: THE APP INITIALIZER
// ============================================

// app.config.ts
export const appConfig: ApplicationConfig = {
  providers: [
    provideHttpClient(),
    provideAppInitializer(() => inject(ConfigService).load()),
  ],
};

// ============================================
// PART 7: USING THE RUNTIME CONFIG
// ============================================

@Injectable({ providedIn: 'root' })
export class ApiService {
  private http = inject(HttpClient);
  private config = inject(ConfigService);

  getUsers(): Observable<User[]> {
    return this.http.get<User[]>(`${this.config.apiUrl}/users`);
  }
}

// ============================================
// PART 8: THE FEATURE FLAG
// ============================================

@Component({
  selector: 'app-dashboard',
  template: `
    @if (config.isFeatureEnabled('newDashboard')) {
      <app-new-dashboard />
    } @else {
      <app-old-dashboard />
    }
  `,
})
export class DashboardComponent {
  config = inject(ConfigService);
}

// ============================================
// PART 9: THE SECURITY RULE
// ============================================

// ❌ Do not store secrets in environment files
export const environment = {
  apiKey: 'sk-1234567890',  // visible in the bundle
};

// ✅ Store secrets on the backend
// The frontend calls the backend
// The backend holds the secret

// ============================================
// PART 10: THE DEPLOYMENT PIPELINE
// ============================================

// The pipeline promotes the same artifact through environments.

// Build once:
ng build --configuration production

// Deploy to staging:
// Replace /assets/config.json with the staging configuration
// Deploy the same bundle

// Deploy to production:
// Replace /assets/config.json with the production configuration
// Deploy the same bundle

// The bundle is identical in every environment.
// The configuration differs.

The ten parts cover the build-time environment files, the build configuration, using the environment, the runtime config, the config service, the app initializer, using the runtime config, the feature flag, the security rule, and the deployment pipeline.


Quick Reference

The Build-Time Approach

ElementPurpose
environment.tsProduction defaults
environment.development.tsDevelopment overrides
fileReplacementsSwap the file per configuration
ng build --configurationSelect the configuration

The Runtime Approach

ElementPurpose
assets/config.jsonExternal configuration
ConfigServiceLoad and expose the config
provideAppInitializerLoad before bootstrap
signal<AppConfig>Reactive config state

The Comparison

AspectBuild-TimeRuntime
ArtifactOne per environmentOne for all
Rebuild requiredYesNo
Config visibleIn the bundleIn the file
ComplexityLowModerate
Use caseSmall projectsMulti-environment pipelines

The Security Rules

RuleReason
No secrets in the bundleThe bundle is public
No secrets in config.jsonThe file is public
Secrets on the backendThe backend is private
Exclude sensitive files from GitThe repository is public

Best Practices

✅ Do This:

// Use the environment object for public configuration
apiUrl: environment.apiUrl                                        // ✅
// Use the runtime config for multi-environment pipelines
provideAppInitializer(() => inject(ConfigService).load())         // ✅
// Use feature flags for gradual rollouts
if (config.isFeatureEnabled('newDashboard')) { ... }              // ✅
# Keep secrets on the backend
# Never commit them to the repository                            // ✅

❌ Don’t Do This:

// Don't hardcode the API URL
apiUrl = 'https://api.example.com'  // not configurable            // ❌
// Don't store secrets in the environment file
apiKey: 'sk-1234567890'  // visible in the bundle                  // ❌
// Don't commit production secrets to Git
// Use the pipeline to inject them                                 // ❌
// Don't read the config after the app is stable
// Load it before bootstrap                                         // ❌

Common Pitfalls

PitfallWhy It HappensFix
Wrong URL in productionHardcoded valueUse the environment or config
Secret in the bundleStored in the environment fileMove to the backend
Rebuild per environmentBuild-time approachUse the runtime config
Config not loadedNo initializerUse provideAppInitializer
Feature flag ignoredNot read at runtimeRead from the config service

Real-World Examples

1. Environment File

export const environment = { production: false, apiUrl: 'http://localhost:3000' };

2. File Replacement

{ "replace": "src/environments/environment.ts", "with": "src/environments/environment.production.ts" }

3. Using the Environment

this.http.get(`${environment.apiUrl}/users`)

4. Runtime Config

{ "apiUrl": "https://api.example.com", "logLevel": "error" }

5. Config Service

async load(): Promise<void> { this.config.set(await firstValueFrom(...)); }

6. App Initializer

provideAppInitializer(() => inject(ConfigService).load())

7. Feature Flag

config.isFeatureEnabled('newDashboard')

8. Build Production

ng build --configuration production

9. Serve Development

ng serve --configuration development

10. Deployment

# Replace /assets/config.json per environment

Visual

The Build-Time Approach

┌──────────────────────────────────────────────┐
│  SOURCE CODE                                 │
│    import { environment } from './environment'│
│                                              │
│  BUILD (production)                          │
│    fileReplacements swaps environment.ts     │
│    with environment.production.ts            │
│       │                                      │
│       ▼                                      │
│  BUNDLE (with production values)             │
│                                              │
│  BUILD (development)                         │
│    fileReplacements swaps environment.ts     │
│    with environment.development.ts           │
│       │                                      │
│       ▼                                      │
│  BUNDLE (with development values)            │
│                                              │
│  One bundle per environment.                 │
│                                              │
└──────────────────────────────────────────────┘

The Runtime Approach

┌──────────────────────────────────────────────┐
│  SOURCE CODE                                 │
│    import { ConfigService } from './config'  │
│                                              │
│  BUILD (once)                                │
│       │                                      │
│       ▼                                      │
│  BUNDLE (no environment values)              │
│       │                                      │
│       ▼                                      │
│  DEPLOY to staging                           │
│    └─ config.json = staging values           │
│       │                                      │
│       ▼                                      │
│  DEPLOY to production                        │
│    └─ config.json = production values        │
│                                              │
│  One bundle for all environments.            │
│                                              │
└──────────────────────────────────────────────┘

The Security Boundary

┌──────────────────────────────────────────────┐
│  FRONTEND (public)                           │
│    ├─ Environment files                      │
│    ├─ config.json                            │
│    └─ The compiled bundle                    │
│                                              │
│  Everything here is visible to the user.     │
│  Store only public values.                   │
│                                              │
│  BACKEND (private)                           │
│    ├─ API keys                               │
│    ├─ Database credentials                   │
│    └─ Secrets                                │
│                                              │
│  The backend holds the secrets.              │
│  The frontend calls the backend.             │
│                                              │
└──────────────────────────────────────────────┘

The App Initializer

┌──────────────────────────────────────────────┐
│  BOOTSTRAP SEQUENCE                          │
│                                              │
│  1. Angular starts                           │
│  2. provideAppInitializer runs               │
│  3. ConfigService.load() fetches config.json │
│  4. Config is stored in the signal           │
│  5. The app becomes stable                   │
│  6. The first component renders              │
│                                              │
│  The config is available before the first    │
│  component renders.                          │
│                                              │
└──────────────────────────────────────────────┘

Summary

ItemValue
Build-time configenvironment.ts, fileReplacements
Runtime configassets/config.json, ConfigService
App initializerprovideAppInitializer
Feature flagRead from the config
Build commandng build --configuration
Serve commandng serve --configuration
Security ruleNo secrets in the frontend
DeploymentOne artifact, config per environment

Key takeaways:

  • Environment configuration keeps the application code constant while the settings vary. The API URL, the feature flags, and the logging level are environment-specific. The application code reads them from a configuration source .
  • The build-time approach uses the environment files and the fileReplacements array. The environment.ts file contains the defaults. The environment.development.ts and environment.production.ts files contain the overrides. The build configuration selects the file .
  • The runtime approach loads the configuration from an external source after the application starts. The assets/config.json file is fetched by the ConfigService. The provideAppInitializer function loads the config before the application bootstraps. The same bundle runs in every environment .
  • The runtime approach enables the “build once, deploy anywhere” principle. The artifact that was tested in staging is the artifact that runs in production. Only the config.json file differs. This is the recommended approach for multi-environment pipelines .
  • Secrets must not be stored in the frontend. The frontend bundle and the config.json file are visible to the user. Secrets belong on the backend. The backend holds the secret and the frontend calls the backend .
  • The .gitignore file should exclude sensitive configuration files. The repository should contain templates with placeholder values. The deployment pipeline injects the real values at build time or at deployment time .
  • Feature flags control the behavior without changing the code. A feature flag in the environment file or the config.json file enables or disables a feature. The application reads the flag and conditionally renders the feature. The flag can be changed without rebuilding the application when the runtime approach is used .

Remember: Environment configuration is the layer that makes the application portable. The build-time approach bakes the values into the bundle. The runtime approach loads them from an external source. The runtime approach is the modern recommendation. The secrets belong on the backend. The frontend holds only public configuration. The goal is the same in every approach: the application code stays constant, the settings vary, and the deployment is predictable.

Was this helpful?


Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!