Q22 of 37 · API testing
How do you organise environment variables and secrets across local/staging/prod API tests?
Short answer
Short answer: Layered config: defaults in code, per-environment files (.env.local, .env.staging) for non-secrets, secrets in a manager (1Password, Vault, AWS Secrets Manager). Never commit secrets. Inject via env vars at runtime; test code reads `process.env.X` only.
Detail
Test environments diverge: local hits localhost or Docker; staging hits cloud staging; CI hits whichever it's targeting. Config sprawl is one of the most common reasons API test suites become unmaintainable.
The layered model:
Layer 1 — Defaults in code:
const config = {
baseUrl: process.env.API_BASE_URL ?? 'http://localhost:3000',
timeout: Number(process.env.API_TIMEOUT ?? 30_000),
};
Layer 2 — Per-environment, non-secret files. Checked into git:
# .env.staging
API_BASE_URL=https://staging-api.example.com
API_TIMEOUT=60000
Test runner picks the file based on NODE_ENV or an explicit flag.
Layer 3 — Secrets, never in git. Pulled at runtime from:
- 1Password for local dev —
op run --env-file=.env.local.tmpl -- npm test. - HashiCorp Vault for self-hosted CI.
- AWS Secrets Manager / GCP Secret Manager for cloud CI.
- GitHub Actions secrets for GHA pipelines (encrypted, scoped per env).
The .env.example file commits placeholders so engineers know what to set:
# .env.example
API_BASE_URL=
API_TOKEN= # Get from 1Password vault "QA secrets"
The principles:
1. Secrets never travel through git, ever. Use .env.local (gitignored) or a secret manager. git-secrets and pre-commit hooks catch accidental commits.
2. Test code reads only process.env.X. Never hardcode endpoints, tokens, or paths. The runtime decides.
3. One source of truth per environment. Don't have credentials in .env and in CI secrets — drift is guaranteed. Pull from one, copy from there.
4. Environment names match the test code. local, staging, prod is enough; don't create dev1, dev2, testenv-3 unless they correspond to real isolated environments.
5. Tests fail loudly on missing config:
if (!process.env.API_TOKEN) {
throw new Error('API_TOKEN required — set in .env.local');
}
Better than tests passing because they default to localhost when staging credentials are missing.
Anti-patterns:
- Hardcoded staging URL in 12 different test files. Refactor to a single
config.ts. - "Test secrets" committed because "they're just test accounts." If they grant any access, they're real secrets.
- Running prod-targeted tests by accident. Add an explicit safety check: refuse to delete in prod even if config points there.
For data-intensive secrets (test card numbers, OAuth credentials), keep them in the secret manager too. Rotation is easier and the secret manager logs access.
// WHAT INTERVIEWERS LOOK FOR
// COMMON PITFALL
// Related questions
How do you handle environment-specific configuration in Cypress (dev/staging/prod)?
Cypress
How do you handle environment-specific config and secret management in Playwright?
Playwright
How do you secure secrets (API keys, credentials) in a CI pipeline running QA tests?
CI/CD & DevOps
A QA engineer discovers that an API key was committed to the test repository 6 months ago and is still in Git history. What is your remediation plan?
Git