CodingNic

From Development to Production

Exercises

From Development to Production 25 min read

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:

javascript
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:

text
{
  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:

text
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:

text
{
  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.