Angularのenvironmentとは、APIのURLなど環境ごとに変わる値をTypeScriptのファイルにまとめ、ビルド時に angular.json の fileReplacements でファイルごと差し替えるしくみです。 ng generate environments でひな形を作り、ng build --configuration 名前 で切り替えます。

この記事では、ひな形の生成から staging 環境の追加までを順番に進め、ビルドした dist の中身を grep して「本当にその値が入ったか」を確かめた結果を載せます。さらに、ビルド時に値を差し込む --define、ビルド後に値を変えられる config.json 方式も同じアプリで動かし、3つの方式を比較表にまとめました。設定名やパスを書き間違えたときに出るエラー文も、実際に出したものを一覧にしています。

確認環境:Linux(x64)、Node.js 24.21.0、Angular CLI 22.2.1 / Angular 22.2.1(ビルダーは @angular/build:application)、ブラウザでの表示確認は Playwright 1.56.0 の Chromium。

Angular environment 設定の早見表

やりたいこと 方法 値が決まる時点
environment のひな形を作る ng generate environments ―
開発用の値でビルドする ng build --configuration development ビルド時
本番用の値でビルドする ng build(既定が production) ビルド時
staging を増やす environment.staging.ts を作り、angular.json の configurations に staging を追加 ビルド時
開発サーバーで staging の値を使う serve の configurations にも staging を追加し、ng serve --configuration staging 起動時
バージョン番号などをコマンドから渡す ng build --define "BUILD_VERSION='1.4.0'" ビルド時
ビルドし直さずに値を変える public/config.json を起動時に fetch で読む ブラウザで開いた時
どの値が入ったか確かめる grep -o '探したい文字列' dist/…/browser/*.js ―
方式 ひとことで 向いている値
fileReplacements(environment ファイル) ファイルを丸ごと差し替える 環境ごとに決まっている設定一式(APIのURL、ログの詳しさ)
--define コード中の識別子を定数に置き換える ビルドのたびに変わる値(バージョン、コミットID)
実行時の config.json 起動時にJSONを取りに行く 配置先ごとに変えたい値(同じビルド成果物を複数の環境で使う)

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Angular environmentで環境変数を切り替える設定手順」

environment ファイルで値が切り替わるしくみ

environment は Angular 専用の特別な機能ではなく、「ビルド時に、あるファイルを別のファイルに置き換える」という fileReplacements の応用です。流れは次のとおりです。

flowchart LR
  A["app.ts<br>import { environment } from '../environments/environment'"] --> B{"ng build --configuration ?"}
  B -->|production| C["environment.ts をそのまま使う"]
  B -->|development| D["environment.development.ts に置き換え"]
  B -->|staging| E["environment.staging.ts に置き換え"]

押さえておきたい点は3つです。

  • アプリのコードは常に environment.ts を import する。environment.development.ts を直接 import すると、置き換えの対象から外れます。
  • environment.ts は「置き換えが起きなかったとき」に使われる。CLI が作る設定では、これが本番用の値になります。
  • 値はビルド結果の JavaScript に文字列として埋め込まれる。あとで見るように、dist を grep すればそのまま見つかります。

手順1:ng generate environments でひな形を作る

ng new で作った直後のプロジェクトには environment ファイルがありません。次のコマンドで作ります。

ng generate environments

実行結果です。

CREATE src/environments/environment.ts (31 bytes)
CREATE src/environments/environment.development.ts (31 bytes)
UPDATE angular.json (2249 bytes)

作られる2つのファイルの中身は、どちらも空のオブジェクトです。

export const environment = {};

angular.json には、development の構成に fileReplacements が追加されます(git diff の該当部分)。

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

production の構成には何も追加されません。 本番ビルドでは置き換えをせず、environment.ts をそのまま使うためです。

手順2:値を書いてコンポーネントから使う

2つのファイルに同じ形のオブジェクトを書きます。

// src/environments/environment.ts(本番用。置き換えが無いときに使われる)
export const environment = {
  name: 'production',
  apiUrl: 'https://api.example.com',
  debug: false,
};
// src/environments/environment.development.ts(開発用)
export const environment = {
  name: 'development',
  apiUrl: 'http://localhost:3000',
  debug: true,
};

使う側は、environment.ts を import するだけです。

// src/app/app.ts
import { Component } from '@angular/core';
import { environment } from '../environments/environment';

@Component({
  selector: 'app-root',
  template: `
    <h1>環境: {{ env.name }}</h1>
    <p id="api">API: {{ env.apiUrl }}</p>
    <p id="debug">debug: {{ env.debug }}</p>
  `,
})
export class App {
  protected readonly env = environment;
}

サービスから使うときも同じで、environment.apiUrl を読んで URL を組み立てます。

手順3:staging 環境を追加する

開発と本番のあいだに、確認用の staging を足します。作業は「ファイルを1つ作る」「angular.json の2か所に書き足す」の2つです。

3-1. environment.staging.ts を作る

// src/environments/environment.staging.ts
export const environment = {
  name: 'staging',
  apiUrl: 'https://stg-api.example.com',
  debug: true,
};

3-2. angular.json の build と serve に staging を足す

architect.build.configurations と architect.serve.configurations の両方に追加します。

           "configurations": {
             "production": { ... },
             "development": { ... },
+            "staging": {
+              "fileReplacements": [
+                {
+                  "replace": "src/environments/environment.ts",
+                  "with": "src/environments/environment.staging.ts"
+                }
+              ],
+              "outputHashing": "all"
+            }
           },
           "defaultConfiguration": "production"
         },
         "serve": {
           "builder": "@angular/build:dev-server",
           "configurations": {
             "production": { "buildTarget": "app:build:production" },
             "development": { "buildTarget": "app:build:development" },
+            "staging": { "buildTarget": "app:build:staging" }
           },

app:build:staging の app はプロジェクト名です。自分の angular.json の projects の直下にある名前に合わせてください。

staging に optimization を書かなかった場合、最適化は有効のままでした。下の実行結果で、staging の main が production と同じ 190.60 kB になっていることから分かります。開発用のように読みやすい出力にしたいときは、development と同じ "optimization": false と "sourceMap": true を staging にも書きます。

手順4:ng build --configuration ごとの出力を確かめる

3つの構成でビルドし、dist に入った URL を grep で数えます。出力先が混ざらないよう、--output-path で分けています。

ng build --configuration production  --output-path dist/production
ng build --configuration development --output-path dist/development
ng build --configuration staging     --output-path dist/staging

production のビルド出力です。

Initial chunk files | Names         |  Raw size | Estimated transfer size
main-I3SLVO63.js    | main          | 190.60 kB |                52.11 kB
styles-5INURTSO.css | styles        |   0 bytes |                 0 bytes

                    | Initial total | 190.60 kB |                52.11 kB

Output location: /home/taro/app/dist/production

(パスの表示は環境に合わせて置き換えています。)

それぞれの dist で、埋め込まれた URL を調べます。

grep -o -h 'https\?://[a-z.-]*example.com|http://localhost:3000' dist/production/browser/*.js | sort | uniq -c
構成 出力された main サイズ grep で見つかった URL
production main-I3SLVO63.js 190.60 kB https://api.example.com(1件)
development main.js(+ main.js.map) 1.35 MB http://localhost:3000(1件)
staging main-2ANM2GV2.js 190.60 kB https://stg-api.example.com(1件)

どの構成でも、入っている URL は1種類だけです。ほかの環境の値はビルド結果に含まれません。配置する前にこの grep を1回流すと、「本番に開発用の URL が入っていた」という取り違えにすぐ気づけます。

ng serve で切り替える

開発サーバーも --configuration で切り替えられます。

ng serve --configuration staging --port 8801

起動後に配信された main の JavaScript を取得して調べると、https://stg-api.example.com が入っていました。ng serve を引数なしで実行したときは、serve の defaultConfiguration(ng new 直後は development)が使われます。

ビルド時に値を差し込む --define

バージョン番号やコミットIDのように、ビルドのたびに変わる値は environment ファイルに書くより、コマンドから渡すほうが扱いやすくなります。--define は、コード中の識別子をビルド時に定数へ置き換えるオプションです。

型宣言を用意する

TypeScript に識別子の存在を伝えるため、src/types.d.ts を作ります。公式ドキュメント(Angular application build system のページ)でも、型チェックのエラーを防ぐためにこのファイルを置くよう案内されています。

declare const BUILD_VERSION: string;
declare const API_URL: string;

コンポーネントでは、ふつうの定数のように読みます。

protected readonly version = BUILD_VERSION;
protected readonly definedApi = API_URL;

コマンドから渡す

ng build --define "BUILD_VERSION='1.4.0'" --define "API_URL='https://def-api.example.com'"

ビルド後に grep すると、両方の値が1件ずつ入っていました。

      1 1.4.0
      1 https://def-api.example.com

文字列は、シェル用の二重引用符の内側に、もう一組の一重引用符が必要です。値は JavaScript の式としてそのまま差し込まれるため、内側の引用符が無いと文字列として解釈されません。引用符を付けずに --define "BUILD_VERSION=1.4.0" と書くと、次のエラーでビルドが止まります。

✘ [ERROR] Invalid define value (must be an entity name or JS literal): 1.4.0

angular.json に書く

構成ごとに決まっている値なら、angular.json の各構成に define を書けます。

"staging": {
  "fileReplacements": [
    {
      "replace": "src/environments/environment.ts",
      "with": "src/environments/environment.staging.ts"
    }
  ],
  "outputHashing": "all",
  "define": {
    "BUILD_VERSION": "'1.4.0-stg'",
    "API_URL": "'https://stg-api.example.com'"
  }
}

3つの構成でビルドして grep した結果です。

コマンド 入っていた BUILD_VERSION
ng build --configuration production 1.4.0
ng build --configuration staging 1.4.0-stg
ng build --configuration development dev
ng build --configuration staging --define "BUILD_VERSION='9.9.9'" 9.9.9

最後の行のとおり、angular.json とコマンドラインの両方に同じ識別子があるときは、コマンドラインが優先されます。公式ドキュメントにも同じ説明があります。

define を渡し忘れるとどうなるか

型宣言だけあって --define も angular.json の define も無い場合、ビルドは通り、ブラウザで開いたときにエラーになります。実際に試すと、ビルドは成功し、画面は真っ白のまま、コンソールに次の行が出ました。

ERROR ReferenceError: BUILD_VERSION is not defined

型宣言は「この名前の値がある」と TypeScript に約束しているだけなので、ビルドでは気づけません。全部の構成に define を書いておくと、渡し忘れを防げます。

ビルドし直さずに値を変える:実行時に config.json を読む

environment ファイルと --define は、どちらも値がビルド結果に埋め込まれます。同じビルド成果物を staging と本番の両方に配置したいときは、起動時に設定ファイルを取りに行く方式が合います。

完成コード

public フォルダに置いたファイルは、そのまま dist にコピーされます。

{ "apiUrl": "https://api.example.com", "featureNewList": false }

設定を読み込んで保持するサービスです。

import { Injectable } from '@angular/core';

export interface RuntimeConfig {
  apiUrl: string;
  featureNewList: boolean;
}

@Injectable({ providedIn: 'root' })
export class AppConfig {
  private config?: RuntimeConfig;

  async load(): Promise<void> {
    const res = await fetch('config.json', { cache: 'no-store' });
    if (!res.ok) {
      throw new Error(`config.json を読めませんでした: ${res.status}`);
    }
    this.config = (await res.json()) as RuntimeConfig;
  }

  get value(): RuntimeConfig {
    if (!this.config) {
      throw new Error('config.json の読み込み前に参照されました');
    }
    return this.config;
  }
}

provideAppInitializer に渡した関数が返す Promise が終わるまで、アプリの起動が待たされます。これで、最初のコンポーネントが描画される時点で設定が読めるようになります。

import {
  ApplicationConfig,
  inject,
  provideAppInitializer,
  provideBrowserGlobalErrorListeners,
} from '@angular/core';
import { AppConfig } from './app-config';

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

コンポーネントでは inject(AppConfig) して値を読みます。environment、--define、config.json の3つを並べて表示する例です。

import { Component, inject } from '@angular/core';
import { environment } from '../environments/environment';
import { AppConfig } from './app-config';

@Component({
  selector: 'app-root',
  template: `
    <h1>環境: {{ env.name }}</h1>
    <p id="api">API: {{ env.apiUrl }}</p>
    <p id="debug">debug: {{ env.debug }}</p>
    <p id="define">version: {{ version }} / defineのAPI: {{ definedApi }}</p>
    <p id="runtime">実行時のAPI: {{ config.value.apiUrl }}</p>
  `,
})
export class App {
  protected readonly env = environment;
  protected readonly version = BUILD_VERSION;
  protected readonly definedApi = API_URL;
  protected readonly config = inject(AppConfig);
}

実行結果:config.json だけを書き換える

--define 付きでビルドした dist を配信し、ブラウザで開いた結果です。

環境: production
API: https://api.example.com
debug: false
version: 1.4.0 / defineのAPI: https://def-api.example.com
実行時のAPI: https://api.example.com

次に、ビルドはせず、dist/app/browser/config.json の中身だけを書き換えます。

{ "apiUrl": "https://stg-api.example.com", "featureNewList": true }

再読み込みした結果です。最後の行だけが変わりました。

環境: production
API: https://api.example.com
debug: false
version: 1.4.0 / defineのAPI: https://def-api.example.com
実行時のAPI: https://stg-api.example.com

environment と --define の値はビルド時のまま、config.json の値だけが入れ替わっています。 コンテナで配る場合も考え方は同じで、ビルド成果物を入れたイメージは1つのまま、配置先ごとに config.json だけを差し替えれば済みます。

config.json が無いとき

config.json を消して開くと、起動時の関数がエラーを投げるため、画面は何も表示されず、コンソールに次の行が出ました。

Failed to load resource: the server responded with a status of 404 (File not found)
ERROR Error: config.json を読めませんでした: 404

サービス側でエラーメッセージを自分で書いておくと、原因がすぐ分かります。fetch に cache: 'no-store' を付けているのは、書き換えた設定がブラウザのキャッシュで以前の内容のままになるのを避けるためです。

3つの方式の比較表

観点 fileReplacements(environment) --define 実行時の config.json
値が決まる時点 ビルド時 ビルド時 ブラウザで開いた時
値を変えるときビルドし直しが要るか 要る 要る 要らない(JSON を書き換えて再読み込み)
1つのビルド成果物(コンテナイメージ)を複数環境で使い回せるか 環境ごとにビルドが必要 環境ごとにビルドが必要 使い回せる(config.json だけ差し替え)
秘密情報を置けるか 置けない(JS に埋め込まれる) 置けない(JS に埋め込まれる) 置けない(誰でも取得できる JSON)
型の確認 オブジェクトの形から自動で推論 types.d.ts に自分で宣言 interface を自分で書く(中身の検査は別途)
値が無いときの気づきやすさ ビルド時にエラー(プロパティ不足) ビルドは通り、ブラウザでエラー 起動時にエラー(自分で書いたメッセージ)
使う場所の制限 どこでも import できる コンポーネントのデコレーター内は置き換え対象外(CLI のヘルプに記載) 起動時の読み込みが終わってから
設定する場所 src/environments/ と angular.json コマンドライン、または angular.json の define public/config.json と起動時の関数

秘密情報はどの方式にも置かない

3つの方式はどれも、値がブラウザに届きます。 environment と --define の値は手順4の grep のとおり JavaScript に文字列で入り、config.json は URL を開けばそのまま読めます。公式ドキュメントの環境設定のページにも、environment ファイルはバンドルに含まれるため API キーなどの秘密情報を置かないように、という注意書きがあります。

外部サービスの秘密鍵が必要な処理は、サーバー側の API に置き、ブラウザからはその API を呼ぶ形にします。

どれを選ぶかの目安

状況 選ぶ方式
環境の数が少なく、環境ごとにビルドしてよい environment ファイル
CI でバージョンやコミットIDを埋め込みたい --define(environment と併用)
同じ成果物を検証してから、そのまま本番へ進めたい config.json
画面の機能を配置先ごとに出し分けたい config.json(フラグを持たせる)
開発中だけログを詳しくしたい environment ファイルの debug など

3つは併用できます。 この記事の完成コードも、環境の基本設定は environment、バージョンは --define、配置先で変える URL は config.json と分けています。

出てくるエラー文と直し方の一覧

設定名やパスを書き間違えたときに、実際に出たメッセージです。

状況 出たメッセージ 直し方
--configuration の名前を打ち間違えた(stagin) Configuration 'stagin' for target 'build' in project 'app' is not set in the workspace. angular.json の build.configurations にある名前と一致させる
ng serve --configuration staging で、serve 側に staging が無い Configuration 'staging' for target 'serve' in project 'app' is not set in the workspace. serve.configurations に "staging": { "buildTarget": "app:build:staging" } を足す
fileReplacements の with のパスが違う The /home/taro/app/src/environments/environment.stg.ts path in file replacements does not exist. ファイル名とパスを実際のものに合わせる
fileReplacements の replace のパスが違う エラーは出ない(置き換えが起きず、environment.ts の値でビルドされる) dist を grep して値を確かめ、replace を import しているファイルのパスに合わせる
環境ファイルの1つにプロパティが足りない TS2339: Property 'debug' does not exist on type '{ name: string; apiUrl: string; }'. すべての環境ファイルに同じプロパティを書く
--define の文字列に内側の引用符が無い Invalid define value (must be an entity name or JS literal): 1.4.0 --define "NAME='値'" の形にする
--define を渡し忘れた ビルドは成功。ブラウザで ReferenceError: BUILD_VERSION is not defined 全構成の define に書くか、コマンドに付ける
config.json が配置されていない ブラウザで Error: config.json を読めませんでした: 404(自分で書いたメッセージ) public/config.json を置く。配置先で差し替えるならファイルの置き場所を確認
Node.js の版が合わない The Angular CLI requires a minimum Node.js version of v22.22.3 or v24.15.0 or v26.0.0. 表示された版以上の Node.js にする

(パスの表示は環境に合わせて置き換えています。)

いちばん気づきにくいのは replace のパス違い

with のパスを間違えるとビルドが止まるので、すぐに気づけます。一方、replace のパスを間違えたときは、ビルドが成功してしまいます。

replace を src/environment/environment.ts(environments の s が抜けている)にして development でビルドした結果です。

Application bundle generation complete.

$ grep -o -h 'https://api.example.com|http://localhost:3000' dist/x/browser/main.js | sort | uniq -c
      1 https://api.example.com

開発用のビルドなのに、本番の URL が入っています。「置き換える元のファイルが import されていなかった」だけなので、CLI から見るとエラーではないためです。構成を足したり書き換えたりしたら、手順4の grep で値を確かめるのが確実です。

プロパティ不足はビルドで見つかる

environment.staging.ts から debug を消してビルドすると、テンプレートで env.debug を使っている箇所でエラーになります。

✘ [ERROR] TS2339: Property 'debug' does not exist on type '{ name: string; apiUrl: string; }'.

    src/app/app.ts:9:32:
      9 │     <p id="debug">debug: {{ env.debug }}</p>
        ╵                                 ~~~~~

置き換えた後のファイルで型チェックが行われるので、その構成でビルドしたときにだけ見つかります。CI で全部の構成をビルドしておくと、配置の直前に気づくことを避けられます。型をそろえたいときは、共通の interface を作って各ファイルで使う方法もあります。

// src/environments/environment.model.ts
export interface Environment {
  name: string;
  apiUrl: string;
  debug: boolean;
}
// 各 environment ファイル
import { Environment } from './environment.model';

export const environment: Environment = {
  name: 'staging',
  apiUrl: 'https://stg-api.example.com',
  debug: true,
};

こう書くと、プロパティが足りないファイルそのものにエラーが出るので、原因の場所が分かりやすくなります。

設定を見直すときのチェックリスト

確認すること 見る場所
アプリのコードが import しているのは environment.ts か import { environment } from '…/environments/environment'
replace のパスが、import しているファイルと同じか angular.json の fileReplacements
with のファイルが実在するか src/environments/
build と serve の両方に構成名があるか angular.json の2か所の configurations
すべての環境ファイルが同じプロパティを持つか 各 environment.*.ts
配置する dist に、想定した URL だけが入っているか grep -o で確認
秘密情報を書いていないか environment、define、config.json のすべて

よくある質問

Q. .env ファイルや process.env は使えますか? A. ブラウザで動く Angular アプリには process.env がありません。ビルド時に値を渡したいときは --define を使います。CI では ng build --define "BUILD_VERSION='$VERSION'" のように、シェルの変数を展開して渡せます。

Q. ng build と ng serve で既定の環境が違うのはなぜですか? A. angular.json の defaultConfiguration が、build は production、serve は development になっているためです。引数なしの ng build は本番用、ng serve は開発用の値になります。

Q. 複数の構成を同時に指定できますか? A. 公式ドキュメントに、ng build --configuration debug,production,customer-facing のようにカンマ区切りで複数を指定できると書かれています。

Q. environment の値を単体テストで差し替えたいときは? A. environment を直接読む箇所をサービス1つにまとめておくと、テストではそのサービスを差し替えるだけで済みます。config.json 方式の AppConfig も同じ考え方で、テストでは load() を呼ばずに値を持たせた代わりのクラスを渡します。

まとめ

  • environment は fileReplacements によるファイルの置き換え。アプリは常に environment.ts を import し、ビルド時に構成に応じたファイルへ差し替わる
  • ng generate environments でひな形と development の置き換え設定ができる。staging は、ファイルを1つ作り build と serve の configurations に足す
  • dist を grep すれば、入った値をすぐ確かめられる。replace のパス違いはエラーにならないので、この確認が効く
  • --define はビルドごとに変わる値に向く。文字列は "NAME='値'" と引用符を二重にし、types.d.ts に宣言を書く
  • ビルドし直さずに変えたい値は config.json。provideAppInitializer で起動時に読み、同じ成果物を複数の環境で使える
  • どの方式でも値はブラウザに届く。秘密情報はサーバー側に置く
  • CI で全構成をビルドすると、プロパティ不足やパス違いに早く気づける

参考・出典

  • Angular 公式ドキュメント「アプリケーション環境の設定」(https://angular.jp/tools/cli/environments):ng generate environments、fileReplacements、複数構成の指定、秘密情報についての注意
  • Angular 公式ドキュメント「Angular application build system」(https://angular.dev/tools/cli/build-system-migration):define オプション、types.d.ts、コマンドライン優先の説明
  • ng build --help(Angular CLI 22.2.1):--define の説明文

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Angular environmentで環境変数を切り替える設定手順」

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?