0Pricing
Node.js Backend Development Bootcamp · レッスン

型安全な環境設定とランタイム検証

スキーマライブラリを使い、環境変数と外部入力をランタイムで検証し、静的型へ推論します。

「型安全な環境設定とランタイム検証」はCoddyKit上の無料Node.js Backend Development Bootcampレッスンです。 これはレッスン3/4です。 下記で完全なレッスンを無料で読むことができます。その後、ブラウザ内の組み込みコードエディタと24時間対応のAIチューターでハンズオン演習できます。 これはNode.js Backend Development Bootcamp学習パスの一部であり、ウェブとCoddyKitアプリ全体で進捗が同期されます。 Node.js Backend Development Bootcampコースには全4レッスンが含まれています。

このレッスンの一部はまだ翻訳されておらず、英語で表示されています。

Why Validate process.env?

In Node.js, every value on process.env is a string or undefined — the runtime gives you no guarantees.

  • process.env.PORT might be "3000", "", or missing entirely.
  • A typo like DATABSE_URL silently yields undefined.
  • TypeScript types process.env as Record<string, string | undefined>, so it can't catch missing keys.

Reading config ad-hoc throughout your app means crashes surface deep inside request handlers — long after startup. The fix: validate once at boot and fail fast with a clear message.

// Untyped, unsafe access scattered everywhere
const port = process.env.PORT;            // string | undefined
const dbUrl = process.env.DATABASE_URL;   // string | undefined

console.log(typeof port);  // "string" or "undefined"
console.log(Number(process.env.MISSING)); // NaN — silent failure

Schema Libraries and Type Inference

A schema library lets you describe the shape and constraints of data once, then validate at runtime AND infer a static TypeScript type from the same definition.

  • Popular choices: Zod, Valibot, ArkType, TypeBox.
  • One source of truth — the schema — produces both the runtime check and the compile-time type.
  • No duplicated interface that can drift out of sync.

We'll use Zod, the most common in the Node.js ecosystem. z.infer<typeof schema> extracts the type the schema validates.

import { z } from "zod";

const UserSchema = z.object({
  id: z.number().int(),
  email: z.string().email(),
});

// Static type inferred from the runtime schema
type User = z.infer<typeof UserSchema>;
// type User = { id: number; email: string }

A First Env Schema

Let's describe the environment our service needs. Because process.env values are always strings, the schema must coerce numeric fields and constrain string fields.

  • z.coerce.number() turns "3000" into 3000.
  • z.enum([...]) restricts a value to a fixed set.
  • Chaining like .min() / .url() adds runtime constraints.

Define the schema in its own module (e.g. src/env.ts) so it's imported once at startup.

import { z } from "zod";

export const EnvSchema = z.object({
  NODE_ENV: z.enum(["development", "test", "production"]),
  PORT: z.coerce.number().int().positive().default(3000),
  DATABASE_URL: z.string().url(),
  JWT_SECRET: z.string().min(32),
});

Parsing at Startup

Call schema.parse(process.env) once, at the top of your entry file. If validation fails, Zod throws a ZodError and the process exits before serving any traffic.

  • parse() returns a fully typed, validated object.
  • Coerced and defaulted values are already applied.
  • Export the result so the rest of the app imports a typed env object instead of touching process.env directly.
import { z } from "zod";
import { EnvSchema } from "./env-schema";

export const env = EnvSchema.parse(process.env);
// env.PORT is number, env.NODE_ENV is a narrowed union

const server = createServer();
server.listen(env.PORT, () => {
  console.log(`Listening on ${env.PORT} in ${env.NODE_ENV}`);
});

safeParse for Friendly Errors

A raw ZodError stack trace is noisy. Use safeParse to get a result object you can format into a readable startup message, then exit deliberately.

  • safeParse returns { success: true, data } or { success: false, error } — it never throws.
  • error.flatten().fieldErrors groups messages per field.
  • Exit with process.exit(1) so orchestrators (Docker, PM2, k8s) see a failed boot.
import { z } from "zod";

const EnvSchema = z.object({
  PORT: z.coerce.number().int().positive(),
  DATABASE_URL: z.string().url(),
});

const parsed = EnvSchema.safeParse(process.env);
if (!parsed.success) {
  console.error("Invalid environment variables:");
  console.error(parsed.error.flatten().fieldErrors);
  process.exit(1);
}
export const env = parsed.data;

A Standalone, Runnable Example

Here is a self-contained demonstration of the validate-then-infer pattern using a plain object instead of process.env, so an online judge can run it with no setup.

It shows coercion, a default, an enum, and a friendly error path — the same techniques you'd apply to real environment loading.

import { z } from "zod";

const Schema = z.object({
  NODE_ENV: z.enum(["development", "production"]).default("development"),
  PORT: z.coerce.number().int().positive(),
});

function loadConfig(raw) {
  const result = Schema.safeParse(raw);
  if (!result.success) {
    throw new Error(JSON.stringify(result.error.flatten().fieldErrors));
  }
  return result.data;
}

const config = loadConfig({ PORT: "8080" });
console.log(config); // { NODE_ENV: 'development', PORT: 8080 }
console.log(typeof config.PORT); // number

Loading .env Files with ESM

Locally you store config in a .env file. The library dotenv reads it into process.env. With ESM and TypeScript, load it before any module that reads config.

  • Modern Node (v20.6+) has a built-in --env-file=.env flag — no dependency needed.
  • If you use dotenv, call dotenv/config at the very top, since ESM imports are hoisted and evaluated first.
  • Never commit real secrets; commit a .env.example documenting required keys.
// Option A: built-in (Node 20.6+), no import needed
// $ node --env-file=.env dist/index.js

// Option B: dotenv — must run first
import "dotenv/config";
import { EnvSchema } from "./env-schema";

export const env = EnvSchema.parse(process.env);

Reusing the Schema for Request Input

The exact same approach validates untrusted runtime input — request bodies, query params, webhook payloads. Network data is just as untyped as process.env.

  • Define a schema per endpoint, then parse the incoming JSON.
  • On failure, respond with 400 instead of crashing.
  • The parsed result is fully typed for the rest of the handler.
import { z } from "zod";

const CreateUserBody = z.object({
  email: z.string().email(),
  age: z.coerce.number().int().min(0).optional(),
});

function handleCreateUser(rawBody, res) {
  const result = CreateUserBody.safeParse(rawBody);
  if (!result.success) {
    res.status(400).json({ errors: result.error.flatten() });
    return;
  }
  const body = result.data; // { email: string; age?: number }
  // ...persist body
}

Transforms and Derived Config

Schemas can transform values during parsing, producing config that's ready to use. This keeps conversion logic next to the validation rule.

  • .transform() maps a validated value into a new shape.
  • Comma-separated env strings become arrays; flags become booleans.
  • The inferred type reflects the transformed output, not the raw input.
import { z } from "zod";

const EnvSchema = z.object({
  // "a.com,b.com" -> ["a.com", "b.com"]
  CORS_ORIGINS: z.string()
    .transform((s) => s.split(",").map((o) => o.trim()))
    .pipe(z.array(z.string().url())),
  // "true"/"false" string -> boolean
  ENABLE_CACHE: z
    .enum(["true", "false"])
    .transform((v) => v === "true")
    .default("false"),
});

Cross-Field Rules with refine

Sometimes validity depends on relationships between fields — e.g. in production a secret must be set, but locally a default is fine. Use .refine() or .superRefine() on the object schema.

  • The check runs after individual fields parse.
  • You attach a custom message and a path so the error points at the right field.
  • This encodes business rules that a flat per-field schema cannot.
import { z } from "zod";

const EnvSchema = z
  .object({
    NODE_ENV: z.enum(["development", "production"]),
    SENTRY_DSN: z.string().url().optional(),
  })
  .refine(
    (e) => e.NODE_ENV !== "production" || !!e.SENTRY_DSN,
    { message: "SENTRY_DSN is required in production", path: ["SENTRY_DSN"] }
  );

Exporting a Typed Config Singleton

The end goal: the rest of your codebase never touches process.env. It imports a single validated, typed env object.

  • Centralizing access means TypeScript autocompletes every key and flags typos at compile time.
  • Refactoring a variable name becomes a single-file change.
  • Tests can import a schema and feed mock objects instead of mutating global state.
// src/config.ts
import "dotenv/config";
import { z } from "zod";

const EnvSchema = z.object({
  NODE_ENV: z.enum(["development", "test", "production"]).default("development"),
  PORT: z.coerce.number().int().positive().default(3000),
  DATABASE_URL: z.string().url(),
});

const parsed = EnvSchema.safeParse(process.env);
if (!parsed.success) {
  console.error("❌ Invalid env:", parsed.error.flatten().fieldErrors);
  process.exit(1);
}

export const env = Object.freeze(parsed.data);
// elsewhere: import { env } from "./config";

Quick Check

Test your understanding of the type-safe config pattern.

Recap

You learned to make configuration and external input type-safe in modern Node.js:

  • Why: process.env and network data are untyped strings; validate once and fail fast.
  • Schemas: a library like Zod gives runtime validation plus inferred static types from one definition (z.infer).
  • Env loading: coerce numbers, constrain with enums/defaults, load .env via --env-file or dotenv/config before reading.
  • safeParse: format error.flatten().fieldErrors and process.exit(1) on failure.
  • Beyond env: reuse schemas for request bodies, add .transform() for derived config and .refine() for cross-field rules.
  • Result: export one frozen, typed env singleton the whole app imports.

よくある質問

「型安全な環境設定とランタイム検証」レッスンは無料ですか?

はい。「型安全な環境設定とランタイム検証」の完全なテキストはこのウェブで無料で読めます。インタラクティブに演習し(組み込みコードエディタと24時間対応のAIチューター)、Node.js Backend Development Bootcampコースの残りをアンロックするには、CoddyKit PROにアップグレードしてください。 Node.js Backend Development Bootcampコースには全4レッスンが含まれています。

「型安全な環境設定とランタイム検証」で何を学びますか?

スキーマライブラリを使い、環境変数と外部入力をランタイムで検証し、静的型へ推論します。 ブラウザで直接実行するハンズオンコードでNode.js Backend Development Bootcampを演習し、24時間対応のAIチューターがレッスンを進める中での質問に答えます。

Node.js Backend Development Bootcampを始めるのに経験は必要ですか?

事前経験は必要ありません。CoddyKitのNode.js Backend Development Bootcampは初級者から上級者向けに構成されているため、ここから始めるか最初から始めて、自分のペースで進むことができます。 これはレッスン3/4です。

「型安全な環境設定とランタイム検証」レッスンにはどのくらい時間がかかりますか?

ほとんどのCoddyKitレッスンは約5~10分かかります。各レッスンはコンパクトでインタラクティブなので、着実に進歩し、ウェブとアプリ全体で正確に前回の場所から再開できます。

このNode.js Backend Development Bootcampレッスンでコードを書いて実行できますか?

はい。すべてのNode.js Backend Development Bootcampレッスンに組み込みコードエディタが含まれているため、ブラウザでリアルコードを書いて実行し、即座のAIフィードバックを取得できます。ローカル設定は不要です。

このコースのすべてのレッスン

  1. CommonJSからネイティブES Modulesへの移行
  2. Nodeバックエンドプロジェクト向けtsconfigの設定
  3. 型安全な環境設定とランタイム検証
  4. tsx、ホットリロード、ソースマップによる高速イテレーション
← Node.js Backend Development Bootcampに戻る