Fullstack CourseLearn by building
Back to week 4

Topic

Configuration & secrets management

Definition

Application configuration is the set of environment-specific values resolved at startup and validated before the app accepts traffic, and secrets are the sensitive subset — signing keys, database credentials — that must never be committed to source control or written to logs.

In simpler words

Config is everything that changes between dev and prod; secrets are the config you must never leak. Both should be checked at boot so a misconfigured app fails immediately, not on the first request.

A typical Nest setup does this well: a shared env schema validates every variable with Zod and its loader throws on a bad environment, while ConfigModule is registered globally. Establish that foundation before adding new config.

After this you can

  • Validate configuration at startup and fail fast on bad values
  • Keep secrets out of source control and out of logs
  • Separate per-environment config without branching in code

Validate config at boot, fail fast

Definition

Startup validation parses the raw environment against a typed schema before the application boots, so a missing or malformed value stops the process with a clear error instead of surfacing as a confusing runtime failure deep in a request.

In simpler words

Check every required variable the moment the app starts — a bad JWT_SECRET should crash boot, not blow up on someone’s login.

A shared env schema (apiEnvSchema) built with Zod, whose loader calls safeParse, throws a formatted list of problems if anything is invalid — the API never starts in a half-configured state.

The schema also encodes rules: JWT_SECRET must be at least 16 characters, PORT is coerced to a number with a default, and NODE_ENV is constrained to a known set.

Fail-fast env parsing

const parsed = apiEnvSchema.safeParse(env);
if (!parsed.success) {
  throw new Error(`Invalid API environment:\n${formatIssues(parsed.error)}`);
}
return parsed.data;

One place validates everything; a typo in an env var is caught at boot with a readable message.

Secrets hygiene: never commit, never log

Definition

A secret is any value that grants access if disclosed — the JWT signing key, the database URL with its password — and it must be supplied through the environment, kept out of version control, excluded from logs and error responses, and rotated when exposure is suspected.

In simpler words

Secrets live in the environment, not in git and not in your logs — and if one leaks, you rotate it.

JWT_SECRET signs every access token; if it leaks, anyone can forge a valid token, so it belongs in the environment and in gitignored .env files — a project ships a .env.example with placeholders, never real values.

AllExceptionsFilter deliberately returns a generic 500 for unexpected errors and logs the detail server-side — never echo a DATABASE_URL or stack trace to a client, and keep secrets out of the log lines too.

Schema treats the secret as required and bounded

JWT_SECRET: z.string().min(16),
DATABASE_URL: z.string().min(1),

A too-short or missing signing secret fails validation at boot instead of quietly weakening every token.

Mistake: hardcoding a secret

// Wrong — secret in source, shipped to git forever
const secret = 'dev-secret-123';

// Right — from validated env, provided per environment
const secret = env.JWT_SECRET;

A committed secret is compromised the moment it is pushed; rotate it and move it to the environment.

Per-environment configuration

Definition

The same codebase must run in development, test, and production with different values and stricter guarantees in production, which a schema expresses by defaulting safe-for-dev values while enforcing production-only rules through conditional validation.

In simpler words

Dev and prod share code but not config — and some rules (like secure cookies) should be mandatory only in production.

apiEnvSchema.superRefine enforces that COOKIE_SECURE must be true when NODE_ENV is production, so an insecure cookie setting cannot reach production even though it is allowed in dev.

Compose and deployment supply these values as real environment variables; a .env.example documents the shape so a new environment is filled in deliberately, not guessed.

Production-only rule

.superRefine((env, ctx) => {
  if (env.NODE_ENV === 'production' && !env.COOKIE_SECURE) {
    ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['COOKIE_SECURE'],
      message: 'COOKIE_SECURE must be true in production' });
  }
});

The environment itself refuses to boot production with an insecure cookie — config encodes a security rule.

Keep in mind

  • Validate all config at boot with a schema so bad values fail fast.
  • Keep secrets in the environment and in .env.example placeholders — never real values in git.
  • Never log or return secrets; redact them in any diagnostic output.
  • Encode production-only guarantees (like secure cookies) as conditional validation.

Test

Check your understanding

At least 10 questions — mix of concept, syntax, practical, and logic. Score ≥80% (enforced by the API) to save progress.

Checking your session…

14 questions · concept 4 · syntax 3 · practical 4 · logic 3

1. What is the point of validating config at startup?
Concept
2. What does packages/env use to validate the environment?
Syntax
3. What counts as a secret here?
Practical
4. Where should secrets live?
Logic
5. What does the repo commit instead of real secret values?
Concept
6. What rule does the JWT_SECRET schema enforce?
Practical
7. How does config differ across environments?
Syntax
8. What production-only rule does the schema encode?
Logic
9. Why not log the whole environment at startup?
Concept
10. What does ConfigModule.forRoot({ isGlobal: true }) provide?
Practical
11. What does this guarantee at boot?
Conceptintermediate
const parsed = apiEnvSchema.safeParse(env);
if (!parsed.success) throw new Error('Invalid API environment');
12. What does this schema line enforce?
Syntaxintermediate
JWT_SECRET: z.string().min(16),
13. What is wrong here?
Practicalintermediate
const secret = 'dev-secret-123';
14. What does this superRefine do?
Logicintermediate
if (env.NODE_ENV === 'production' && !env.COOKIE_SECURE) ctx.addIssue({ /* ... */ });

Checking your session…