A web application's CI pipeline bakes the API base URL into the JavaScript bundle at build time, so staging and production each get a separately built bundle. How would you restructure this so one artifact serves both environments?
answer
- build time owns code, deploy time owns environment
- if changing it needs a compile, wrong side
- one bundle, config resolved late
- fetched config file or start-up substitution
- anything in the browser is public
basics
~20 sTake the URL out of the build so one bundle serves every environment, and supply it at deploy time — substituted into the served files at start-up, fetched from a per-environment config file, or derived from the serving origin.
solid answer
~50 sThe rule is that build time owns code and dependencies, and deploy time owns anything that differs per environment. For a server process that is easy: read the URL from an environment variable or a mounted config file and validate it at start-up so a missing value fails fast rather than at the first request. A browser bundle is harder because it is static files, so pick one of three real options — substitute a placeholder into the served files when the container or upload step runs, have the app `fetch('/config.json')` at boot with the hosting layer serving a per-environment copy, or derive the API origin from `window.location` when the API is served under the same host. Whichever you pick, remember that anything reaching the browser is public, so this mechanism is for configuration, never for secrets.
code
bash · 13 lines#!/bin/sh
set -e
# Fail fast: a missing endpoint is a deploy error, not a runtime surprise.
: "${API_BASE_URL:?API_BASE_URL is required}"
# The build emitted a placeholder; resolve it into a small runtime config file
# so the hashed application chunks are never rewritten.
cat > /usr/share/nginx/html/config.json <<EOF
{ "apiBaseUrl": "${API_BASE_URL}" }
EOF
exec nginx -g 'daemon off;'go deeper
Know that endpoints and credentials should be supplied to the application rather than compiled into it, and be able to name environment variables or a mounted config file as the usual mechanism.
Explain why a build-time variable is still baked in, and walk through at least two ways a static browser bundle can obtain per-environment values without being rebuilt.
Bring in the operational edges: failing fast on missing configuration, caching rules for a fetched config document, and the hash and integrity consequences of rewriting built files.
Own the standard across services — one configuration channel, a declared schema, secrets never in client artifacts — and decide the rare cases where a compile-time difference is worth the extra artifact.
## Why baked configuration forces a rebuild If a value that differs per environment is fixed while the bundle is being produced, then producing a bundle for another environment necessarily means running the build again. That is not a small inconvenience — it converts the whole delivery path from "promote one artifact" to "build one artifact per environment", so the thing staging tested is by definition not the thing production runs. Config-in-the-artifact is the single most common reason a team cannot adopt build-once. ## The boundary to draw - **Build time owns**: source, dependencies, the target platform, anything the compiler or bundler genuinely needs to produce bytes. - **Deploy or start-up time owns**: endpoints, hostnames, credentials, tenant identifiers, log levels, sizing, feature toggles, anything with the word *environment* in its description. A useful test: if changing the value requires a fresh compile, it is on the wrong side of the line. ## Server-side processes Straightforward — the process reads configuration from its environment or from a file mounted next to it, and the deployment supplies a different one per environment. Two habits make it robust: 1. **Validate at start-up and fail fast.** A process that boots happily with a missing API base URL and then fails on the first user request has turned a deploy-time error into an incident. 2. **Keep the schema in the artifact, the values outside.** The application ships knowing what settings exist, their types and their defaults; the environment supplies only the values. ## Browser bundles: three real options A single-page app is static files, so there is no process to read an environment variable. The workable patterns: **1. Substitute at serve time.** The build emits a placeholder such as `__API_BASE_URL__`, and the container entrypoint or the upload step rewrites it before the files are served (`sed` or `envsubst`). One artifact, resolved late. The cost: you are editing files after they were built, so any content hash, cache-busting filename or Subresource Integrity value computed at build time no longer matches the served bytes — keep the substituted values in a single small file rather than rewriting hashed application chunks. **2. Fetch config at boot.** The app requests a small document from its own origin before it renders: ```javascript const res = await fetch('/config.json', { cache: 'no-store' }); const { apiBaseUrl } = await res.json(); ``` The hosting layer serves a different `config.json` per environment. This keeps the built assets byte-identical everywhere and leaves their hashes intact. The cost is one extra round trip before first render, and the config document must be served with `Cache-Control: no-store` (or an equally short policy) or a stale copy will point a redeployed app at the old backend. **3. Derive it.** If the API is reachable under the same host as the app, compute the origin from `window.location` and configure nothing at all. Fewest moving parts; only works when your routing genuinely gives you that. ## Public versus secret Every one of these mechanisms puts the value in front of the user. Injecting a value at container start-up does not make it a secret — it is in the JavaScript the browser downloads. Client configuration is public configuration: API hostnames, public analytics keys, feature toggles. Anything that must stay private belongs behind a server the browser talks to, and never in the bundle by any injection route. ## What may legitimately stay at build time The target platform, obviously. Compile-time elimination of code that must not ship at all (a debug-only panel you never want in the production bundle) is a real reason to differ — but recognise the cost: you are now shipping a different artifact than you tested, and the correct default is a runtime flag whose value comes from the same configuration channel. ## The payoff Once configuration is external, the same bundle you tested against a staging API can be pointed at production by changing a deployed value, rollback is a redeploy of a stored artifact rather than a rebuild, and "it worked in staging" starts to be a real statement about the bytes in production.
- Why does rewriting the built JavaScript files at container start cause trouble with caching or Subresource Integrity?Both rely on a hash of the file computed at build time. Editing the served bytes afterwards means the content no longer matches the filename hash or the `integrity` attribute, so browsers may reject the script or serve a cached copy of different content. Confine substitution to one small unhashed file — or emit a separate `config.json` — and leave the application chunks untouched.
- How would you stop a deployment from succeeding when a required configuration value is missing?Validate the whole configuration schema at process start and exit non-zero on anything missing or malformed, so the new instance never becomes healthy and the rollout stalls rather than serving broken traffic. For static frontends, have the deploy step verify the config document it just wrote before flipping traffic to the new version.
- Does injecting the value at start-up make it safe to put an API key in a browser bundle?No. Late injection changes when the value is placed, not who can read it — anything the browser downloads is visible to the user. Client-side configuration is public by construction. A credential that must stay private has to live server-side, with the browser calling an endpoint that holds it.
saying these in an interview costs you the question
- Environment variables read during the build count as runtime config
- Injecting a value at start-up keeps it secret from users
- Each environment obviously needs its own bundle
- Config differences are too small to justify the plumbing
- Minified bundles hide the values inside them