Create your own
Lesson illustration

Environment-Specific API Gateway Configuration Without Hardcoding

Good to see you again. In the previous lesson, you verified the mock banking API independently with curl and Chrome DevTools. That gave you a working local target at http://localhost:3000, but an Angular service must not embed that address in its source code. The same service should work unchanged against a local mock, a staging gateway, or the production gateway.

This lesson establishes one clear configuration boundary: environment files select the API gateway base URL at build time, Angular provides that value once at application bootstrap, and services inject it. You will also distinguish safe public configuration from secrets that must never enter an SPA bundle.


Environment files are build-time public configuration

Angular environment files are TypeScript source files selected by the CLI build configuration. They are not protected runtime environment variables, and they do not read a server’s operating-system environment automatically.

For this project, the intended separation is:

Deployment targetExample gateway base URLSource of value
Local developmenthttp://localhost:3000environment.development.ts
Local Spring gateway, when availablehttp://localhost:8080environment.development.ts
StagingA team-provided staging gateway URLA staging environment file
ProductionA team-provided production gateway URLBase environment.ts

The first local value matches the JSON Server mock from the previous lesson. When the real local Spring Cloud Gateway becomes available, you change one configuration value to http://localhost:8080; account and payment services remain untouched.

Build environments

Read Angular’s official environment-build guide to understand file replacement and the visibility boundary of environment configuration.

In “Configure environment-specific defaults,” focus on why target-specific source files exist. Then, in “Using environment-specific variables in your app,” read the file-replacement explanation. Return to the warning in the first section and read the security warning carefully: it is a non-negotiable constraint for banking software.

Angular’s default approach is a compile-time substitution:

  1. Application code always imports the suffix-free path, src/environments/environment.
  2. The development build replaces that file with environment.development.ts.
  3. The production build uses the base environment.ts unless its build configuration specifies another replacement.
  4. The selected values become part of the JavaScript sent to the browser.

This is broadly similar to a frontend build selecting a Vite configuration value, but Angular’s standard mechanism is explicitly based on source-file replacement. It is useful for public, deploy-target-specific values such as gateway origins, public feature flags, and logging levels.

It is not a way to conceal values from users.


Generate and type the environment configuration

From the root of your Angular application, generate the standard Angular environment setup:

ng generate environments

This command creates the src/environments/ directory and updates the relevant build configurations in angular.json. Inspect the generated output rather than assuming an older tutorial’s filenames. With the current Angular guidance, you will commonly have this structure:

src/
  environments/
    environment.ts
    environment.development.ts

Create one small shared type so every environment must provide the same fields. Add src/environments/environment.model.ts:

export interface EnvironmentConfig {
  readonly production: boolean;
  readonly apiGatewayUrl: string;
}

Now configure the development target in src/environments/environment.development.ts:

import type { EnvironmentConfig } from './environment.model';

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

The URL has no trailing slash. Keep that convention throughout the project so service URLs consistently take the form:

base URL + /resource path

For now, the local development environment targets JSON Server. If you later run the project’s actual local gateway, make the one-line change below:

apiGatewayUrl: 'http://localhost:8080'

Configure the default production environment in src/environments/environment.ts:

import type { EnvironmentConfig } from './environment.model';

export const environment: EnvironmentConfig = {
  production: true,
  apiGatewayUrl: 'https://gateway.example.invalid'
};

gateway.example.invalid is deliberately non-routable. Replace it only with the real production gateway URL supplied through the organization’s deployment process. Do not guess a production hostname from a mock API, an old ticket, or a frontend convention.

The type annotation catches configuration drift early. For example, if a staging file omits apiGatewayUrl, TypeScript reports an error before the application builds.

Check the generated replacement configuration

Open angular.json and locate your project’s build target. The generated development configuration should contain the equivalent of:

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

Do not copy this fragment blindly into the root of angular.json; retain the project name and nesting produced by your CLI. The key fact is that application code imports environment.ts, while the CLI substitutes the development file when it builds or serves the development target.


Make the gateway URL an application dependency

Importing environment directly inside every service would meet the minimum requirement of avoiding hard-coded URLs. However, it ties data-access services to a particular configuration-file mechanism. A stronger application design provides the gateway URL once at bootstrap and makes services explicitly depend on it.

Angular’s InjectionToken is designed for globally injected non-class values such as strings and configuration objects.

Defining dependency providers - Angular

Read Angular’s dependency-injection guidance for non-class dependencies. This explains why a URL should be represented by a token rather than pretending that a TypeScript interface or string name is itself injectable.

In “Automatic provision for non-class dependencies,” read the InjectionToken definition. Notice that a token is an object identity used by Angular’s injector; its descriptive string is only for debugging.

Create src/app/core/config/api-gateway-url.token.ts:

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

export const API_GATEWAY_URL = new InjectionToken<string>(
  'api.gateway.url'
);

Then update your existing src/app/app.config.ts. Preserve providers already generated for routing or animations, and add the imports and providers shown below:

import { ApplicationConfig } from '@angular/core';
import { provideHttpClient } from '@angular/common/http';
import { provideRouter } from '@angular/router';

import { environment } from '../environments/environment';
import { API_GATEWAY_URL } from './core/config/api-gateway-url.token';
import { routes } from './app.routes';

export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(routes),
    provideHttpClient(),
    {
      provide: API_GATEWAY_URL,
      useValue: environment.apiGatewayUrl
    }
  ]
};

If provideHttpClient() is already present in your application configuration, keep the existing one rather than registering it twice.

This creates a single, application-wide gateway URL. The relationship is now deliberate:

LayerResponsibility
Environment fileSelects a public URL for a build target
app.config.tsProvides the selected URL to Angular’s root injector
API_GATEWAY_URLNames the dependency without coupling consumers to environment files
API serviceCombines the injected base URL with its resource path

A component should not provide this token itself. A component-level provider would create a separate configuration scope, which could make one feature call a different gateway than the rest of the application.


Use the injected value in an API service

Here is a focused account service using the local mock contract from the prior lesson. The DTO will become more detailed in the strict HTTP module; its purpose here is to demonstrate where URL responsibility belongs.

import { inject, Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';

import { API_GATEWAY_URL } from '../core/config/api-gateway-url.token';

export interface AccountSummary {
  readonly id: string;
  readonly nickname: string;
  readonly iban: string;
  readonly availableBalance: number;
  readonly currency: string;
}

@Injectable({ providedIn: 'root' })
export class AccountApiService {
  private readonly http = inject(HttpClient);
  private readonly gatewayUrl = inject(API_GATEWAY_URL);

  listAccounts(): Observable<readonly AccountSummary[]> {
    return this.http.get<readonly AccountSummary[]>(
      `${this.gatewayUrl}/accounts`
    );
  }
}

Notice what is and is not in this service:

  • The resource path /accounts belongs here because this service owns account-related API operations.
  • The gateway base URL does not belong here because deployment configuration owns it.
  • Neither http://localhost:3000 nor a staging or production hostname appears in the service.
  • The service receives a string through Angular dependency injection, so it can operate with whichever environment the build selected.

For a gateway that exposes /api as part of its public route, place that stable prefix in the configuration value:

apiGatewayUrl: 'https://gateway.company.example/api'

The service still requests /accounts, producing the intended /api/accounts path. Avoid scattering /api prefixes across unrelated services; that merely recreates the configuration problem at a smaller scale.


Build and verify both targets

Start your mock API as you did in the previous lesson:

cd mock-api
npx json-server db.json

From the Angular project root, serve the development build:

ng serve --configuration development

When an account component calls AccountApiService.listAccounts(), Chrome DevTools Network should show a request to:

http://localhost:3000/accounts

The next modules will build the component and RxJS binding that invoke this service. At this stage, confirm that the configuration compiles under both targets:

ng build --configuration development
ng build --configuration production

A production build succeeding does not mean its placeholder gateway is reachable. It proves that the production configuration has the same required shape and that the application can be compiled with that target selected.

Before committing, inspect these items:

  • environment.development.ts contains the local mock or approved local gateway URL.
  • environment.ts contains only the approved production target value.
  • app.config.ts provides API_GATEWAY_URL once at application scope.
  • Services inject API_GATEWAY_URL; they do not embed hostnames.
  • angular.json contains the expected development file replacement.
  • No user interface, service, or console output exposes a credential.

Public configuration is not a secret store

The gateway URL is normally safe to place in the bundle. Users must be able to discover where their browser sends requests; hiding an API hostname is not a security control.

The following values must never be put in environment.ts, environment.development.ts, frontend source code, browser storage as “configuration,” or a public runtime JSON file:

  • Database credentials and internal service credentials
  • Private third-party API keys
  • JWT signing keys or encryption keys
  • A Keycloak or OAuth client secret
  • Real bearer access tokens or refresh tokens
  • Cloud access keys and secret keys

An Angular SPA is a public OAuth client. In a later module, the application will use Authorization Code with PKCE rather than attempting to protect a client secret in JavaScript. The gateway and backend services remain responsible for validating tokens and enforcing authorization; the browser’s environment selection is never an authorization boundary.

If an organization needs to deploy one identical frontend bundle to multiple environments while changing only the gateway URL, it may use a runtime-loaded public configuration file or a server-side proxy. That is a deployment architecture decision. It still does not make the configuration secret, and it should not be introduced casually because application startup must handle configuration-load failure. For this GCB-WMP practice application, Angular’s standard build-time environments are the right starting point.


Key takeaways

Angular environment files select public, build-specific values through CLI file replacement. They are suitable for a gateway base URL, but every value in them is visible to browser users and must be treated as public.

Keep the suffix-free environment import at the application configuration boundary. Provide environment.apiGatewayUrl through an InjectionToken in app.config.ts, then inject that token in API services. Services own resource paths such as /accounts; environment configuration owns the gateway host and stable gateway prefix.

You have now completed the Angular workspace orientation module: local tooling, standalone workspace structure, React-to-Angular mapping, mock API verification, and safe API-target configuration. Next, you will make a focused revision of modern JavaScript and TypeScript patterns that will support the DTOs and services used throughout the banking application.

Can't find a good explanation? Sign up and we'll make it for you

Sign up