型安全な環境設定とランタイム検証
スキーマライブラリを使い、環境変数と外部入力をランタイムで検証し、静的型へ推論します。
「型安全な環境設定とランタイム検証」は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.PORTmight be"3000","", or missing entirely.- A typo like
DATABSE_URLsilently yieldsundefined. - TypeScript types
process.envasRecord<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 failureSchema 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
interfacethat 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"into3000.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
envobject instead of touchingprocess.envdirectly.
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.
safeParsereturns{ success: true, data }or{ success: false, error }— it never throws.error.flatten().fieldErrorsgroups 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); // numberLoading .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=.envflag — no dependency needed. - If you use
dotenv, calldotenv/configat the very top, since ESM imports are hoisted and evaluated first. - Never commit real secrets; commit a
.env.exampledocumenting 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
parsethe incoming JSON. - On failure, respond with
400instead 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
pathso 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.envand 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
.envvia--env-fileordotenv/configbefore reading. - safeParse: format
error.flatten().fieldErrorsandprocess.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
envsingleton 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フィードバックを取得できます。ローカル設定は不要です。
このコースのすべてのレッスン
- CommonJSからネイティブES Modulesへの移行
- Nodeバックエンドプロジェクト向けtsconfigの設定
- 型安全な環境設定とランタイム検証
- tsx、ホットリロード、ソースマップによる高速イテレーション