Running Angular with Different Environments Per Run
Your Angular app needs to talk to different backends depending on the situation. Local development, staging, production, and sometimes no backend at all — just mocked JSON files. The default environment.ts approach works until it doesn’t. Let’s fix that.
The Problem with Build-Time Environments
Angular’s built-in environment files (environment.ts, environment.prod.ts) are baked in at build time. You set a fileReplacements entry in angular.json, and the CLI swaps one file for another during the build.
export const environment = {
production: false,
apiUrl: 'http://localhost:3000/api',
};
This works for two environments. Maybe three. But then you need a staging config, a QA config, a mock config. Suddenly you’re managing five environment files that are 90% identical. And every config change requires a full rebuild.
One build per environment. Slow to switch. Hard to scale beyond 2-3 configs. Config changes need rebuilds.
One build, many configs. Switch instantly via CLI flags or env vars. Add new environments without touching code.
The better approach: load configuration at runtime.
Runtime Configuration with Environment Variables
The idea is simple. Pass environment variables when you run ng serve, and inject them into your app at startup.
First, create a config loader that reads from a JSON file served alongside your app.
export interface AppConfig {
apiUrl: string;
useMocks: boolean;
logLevel: 'debug' | 'info' | 'warn' | 'error';
}
let config: AppConfig;
export async function loadConfig(): Promise<AppConfig> {
const res = await fetch('/assets/config.json');
config = await res.json();
return config;
}
export function getConfig(): AppConfig {
return config;
}
Wire it up in your app.config.ts using APP_INITIALIZER:
export const appConfig: ApplicationConfig = {
providers: [
{
provide: APP_INITIALIZER,
useFactory: () => () => loadConfig(),
multi: true,
},
],
};
Now your app reads config at startup from /assets/config.json. No rebuild needed.
Switching Configs Per Run
Create separate config files for each environment:
src/assets/
├── config.json # default (local dev)
├── config.staging.json
├── config.mock.json
└── config.prod.json
Then use a simple script to swap the active config before running:
import { cpSync } from 'fs';
const env = process.argv[2] || 'dev';
const source = `src/assets/config.${env}.json`;
cpSync(source, 'src/assets/config.json');
console.log(`Using config: ${env}`);
Add npm scripts to make this easy:
{
"scripts": {
"start": "ng serve",
"start:staging": "ts-node scripts/set-env.ts staging && ng serve",
"start:mock": "ts-node scripts/set-env.ts mock && ng serve"
}
}
Now npm run start:mock swaps the config and starts the dev server. One command.
Setting Up a Local Mock Server
When the real backend isn’t ready — or you just want to work offline — you need a local mock server. json-server is the fastest way to get one running.
Install it:
npm install -D json-server
Create a mocks/ directory at the project root with your mock data:
mocks/
├── db.json
├── routes.json
└── collections/
├── users.json
└── products.json
Your db.json is the single source of truth for the mock API:
{
"users": [
{ "id": 1, "name": "Alice", "email": "alice@example.com", "role": "admin" },
{ "id": 2, "name": "Bob", "email": "bob@example.com", "role": "user" }
],
"products": [
{ "id": 1, "name": "Widget", "price": 29.99, "stock": 150 },
{ "id": 2, "name": "Gadget", "price": 49.99, "stock": 42 }
]
}
Use routes.json to map your real API paths to json-server’s flat structure:
{
"/api/v1/users": "/users",
"/api/v1/users/:id": "/users/:id",
"/api/v1/products": "/products"
}
Start the mock server:
npx json-server mocks/db.json --routes mocks/routes.json --port 3100
You now have a REST API at http://localhost:3100 that supports GET, POST, PUT, PATCH, and DELETE out of the box. No code required.
Structuring Mock Data
Keep your mock data realistic but minimal. A few tips that save headaches down the road.
Separate concerns by domain. Don’t dump everything into one giant db.json. Use a build script to merge individual collection files:
import { readFileSync, writeFileSync, readdirSync } from 'fs';
const collectionsDir = 'mocks/collections';
const db: Record<string, unknown> = {};
for (const file of readdirSync(collectionsDir)) {
const name = file.replace('.json', '');
db[name] = JSON.parse(readFileSync(`${collectionsDir}/${file}`, 'utf-8'));
}
writeFileSync('mocks/db.json', JSON.stringify(db, null, 2));
This way each team member can edit their own collection file without merge conflicts on one massive JSON file.
Combining Everything
Here’s the full workflow. Your config.mock.json points to the local mock server:
{
"apiUrl": "http://localhost:3100/api/v1",
"useMocks": true,
"logLevel": "debug"
}
Your config.json (default dev) points to the real backend:
{
"apiUrl": "https://api-dev.yourapp.com/v1",
"useMocks": false,
"logLevel": "debug"
}
Add a combined npm script that starts both the mock server and Angular:
{
"scripts": {
"mock:build": "ts-node scripts/build-mock-db.ts",
"mock:server": "json-server mocks/db.json --routes mocks/routes.json --port 3100",
"start:mock": "npm run mock:build && ts-node scripts/set-env.ts mock && concurrently \"npm run mock:server\" \"ng serve\""
}
}
One command — npm run start:mock — and you have Angular running against a local mock backend. No network dependency. No waiting for the backend team.
Using the Config in Services
Inject the config wherever you need it. Here’s a typical service pattern:
@Injectable({ providedIn: 'root' })
export class ApiService {
private http = inject(HttpClient);
private baseUrl = getConfig().apiUrl;
getUsers() {
return this.http.get<User[]>(`${this.baseUrl}/users`);
}
getProduct(id: number) {
return this.http.get<Product>(`${this.baseUrl}/products/${id}`);
}
}
The service doesn’t know or care whether it’s talking to a real server or json-server. Same code, different config. That’s the point.
The Practical Takeaway
Stop creating a new environment.*.ts file every time you need a new config. Load config at runtime from a JSON file, swap it with a script, and treat it like data instead of code.
For mocking, json-server gives you a full REST API from a JSON file in under five minutes. Pair it with runtime config and you can develop your entire frontend without the backend running.
The workflow is: one build artifact, many configs, mock server when you need it. Your Angular app shouldn’t need to know where it’s deployed. It just reads the config it’s given and gets to work.
Sources:
- json-server — full fake REST API with zero coding
- Angular CLI Configuration — official Angular CLI documentation