Exercises
Objectives
By the end of this lesson, you should be able to:
- Combine environment-aware configuration and startup validation into a single function
- Add a stricter rule for production specifically, using Zod’s
.refine() - Confirm the same rule behaves correctly across three different environments
⚠️ A note on verification: every command and every output in this lesson was actually run.
Exercise: A Complete, Validated Startup Configuration
a) Combine validation and per-environment defaults into one function:
const { z } = require('zod');
const envSchema = z.object({
NODE_ENV: z.enum(['development', 'test', 'production']).default('development'),
PORT: z.string().regex(/^\d+$/).default('3000'),
JWT_SECRET: z.string().min(16),
DATABASE_URL: z.string().min(1)
}).refine(
(data) => data.NODE_ENV !== 'production' || data.JWT_SECRET.length >= 32,
{ message: 'JWT_SECRET must be at least 32 characters in production', path: ['JWT_SECRET'] }
);
function loadAndValidateConfig() {
const result = envSchema.safeParse(process.env);
if (!result.success) {
console.error('Invalid environment configuration:');
for (const issue of result.error.issues) {
console.error(` ${issue.path.join('.')}: ${issue.message}`);
}
throw new Error('Refusing to start with invalid environment configuration');
}
const env = result.data;
const perEnv = {
development: { logLevel: 'debug', dbPoolSize: 2 },
test: { logLevel: 'silent', dbPoolSize: 1 },
production: { logLevel: 'info', dbPoolSize: 20 }
};
return { ...env, port: Number(env.PORT), ...perEnv[env.NODE_ENV] };
}
module.exports = { loadAndValidateConfig };
.refine() adds a rule spanning more than one field, NODE_ENV and JWT_SECRET together, a 20-character secret is fine in development, where the stakes of a leak are lower, the same 20-character secret is rejected in production, where they aren’t.
b) A 20-character secret in development, should pass:
{
NODE_ENV: 'development',
PORT: '3000',
JWT_SECRET: '01234567890123456789',
DATABASE_URL: 'postgresql://localhost/dev',
port: 3000,
logLevel: 'debug',
dbPoolSize: 2
}
c) The same 20-character secret in production, should fail:
Invalid environment configuration:
JWT_SECRET: JWT_SECRET must be at least 32 characters in production
Threw: Refusing to start with invalid environment configuration
d) A 40-character secret in production, should pass:
{
NODE_ENV: 'production',
PORT: '3000',
JWT_SECRET: '0123456789012345678901234567890123456789',
DATABASE_URL: 'postgresql://prod-host/db',
port: 3000,
logLevel: 'info',
dbPoolSize: 20
}
Same schema, same function, three different environments, each producing exactly the correct outcome, without a single manual if (process.env.NODE_ENV === 'production') check written anywhere in application code, the schema itself encodes the rule.
e) Extend it. Add a rule requiring DATABASE_URL to start with postgresql:// specifically (z.string().startsWith('postgresql://')), and confirm a mysql:// URL is rejected with a clear message.
f) Reflect. Explain, in one or two sentences, why encoding “requirements get stricter in production” directly into the validation schema is more reliable than a comment in the code reminding a developer to configure production carefully.
Recap
This module replaced implicit, easy-to-get-wrong environment handling with an explicit, validated system: a single configuration function keyed by NODE_ENV, per-environment .env files for local development, and startup validation that refuses to run with a missing or insufficiently strong secret, catching a real category of production incident before the application ever accepts a single request.
Next module: process management, keeping a correctly configured application actually running, reliably, once it’s live.