🤦‍♂️

環境変数の呼び出し方、結局どう書けばいいの?(Node.js/Next.js編)

2024/12/11に公開

はじめに

社内のプロジェクト間でも、環境変数の値にアクセスする際の書き方が統一されていないことはありませんか?私自身、その時々でいつも微妙に違う書き方になってしまっている事が多いです。
本記事では、よくある記述とアプローチの1つとして、最近書いたコードを紹介します。

追記: 2024/12/19
少し修正したのを npm package にしました

https://www.npmjs.com/package/dotenv-zod-validator

よくあるケース

環境変数

.env
MY_VALUE="xxx"

1. process.env.*** を直接記述する

xxx.config.ts
const text: string = process.env.MY_VALUE || "";
  • ※ 未定義時の考慮が必要

2. 環境変数を ***.d.ts に型定義する

global.d.ts
declare namespace NodeJS {
	interface ProcessEnv {
		MY_VALUE: string;
	}
}
xxx.config.ts
const text: string = process.env.MY_VALUE;
  • 冗長なデフォルト値の記述が不要になる
  • ※ 未定義時の考慮が必要

3. 環境変数が未定義の場合にエラーを投げる

xxx.config.ts
if (process.env.MY_VALUE === undefined) {
	throw new Error("MY_VALUE is not defined");
}

const text: string = process.env.MY_VALUE;
  • ※ 必須の環境変数ごとに記述が必要

X. その他ケース:...

...

最近書いたケース

環境変数

.env
NODE_ENV="development"
PORT="3000"
TRANSFORMED_VAR="lowercase"
BOOLEAN_FLAG="true"

サンプルコード

プロジェクトにzodが使われていることが多いので、環境変数のバリデーションに使用しました。

用意した Validate 関数(process.env向け)
libs/dotenv.ts
import type { ZodSchema } from "zod";
import { z } from "zod";

export function validateEnv<T extends ZodSchema>(
	schema: T,
	env: NodeJS.ProcessEnv | Record<string, string | undefined> = process.env,
): z.infer<T> {
	const result = schema.safeParse(env);
	if (!result.success) {
		console.error(
			"Environment variable validation failed:",
			result.error.format(),
		);
		throw new Error("Invalid environment variables");
	}
	return result.data;
}

// Custom methods
const envNumber = () =>
	z.preprocess((v) => Number(v) || 0, z.number().int().nonnegative());
const envBoolean = () => z.preprocess((v) => Boolean(v), z.boolean());

const g = {
	envObject: z.object,
	envEnum: z.enum,
	envString: z.string,
	envNumber,
	envBoolean,
};

export { g };

近しい記事:https://catalins.tech/validate-environment-variables-with-zod/

  • .env向けのカスタムメソッド(string | number | boolean | enum)をexport

利用例

usage.ts
import { g, validateEnv } from "...";

const schema = g.envObject({
    NODE_ENV: g.envEnum(["development", "production", "test"]),
    PORT: g.envNumber(),
    TRANSFORMED_VAR: g.envString().transform((val) => val.toUpperCase()),
    OPTIONAL_VAR: g.envString().optional(),
    BOOLEAN_FLAG: g.envBoolean(),
});

export const env = validateEnv(schema);
// NODE_ENV: "development"
// PORT: 3000
// TRANSFORMED_VAR: "LOWERCASE"
// BOOLEAN_FLAG: true
  • 型チェック型変換(必要に応じて)するスキーマを定義
  • 環境変数の値にアクセスする際はバリデーション関数(validateEnv)を通した値を利用する
    • 型安全型変換

Next.js編

Client ComponentServer Componentからアクセスできる環境変数ごとにスキーマを定義

環境変数

.env
NEXT_PUBLIC_MY_VALUE="xxx"
MY_SECRET="***"

Client Component

utils/dotenv.public.ts
import { g, validateEnv } from "@/libs/dotenv";

export const schema = g.envObject({
	NEXT_PUBLIC_MY_VALUE: g.envString(),
});

export const env = validateEnv(schema, {
	/**
	 * Next.js will replace process.env.customKey with 'my-value' at build time. Trying to destructure process.env variables won't work due to the nature of webpack
	 * https://nextjs.org/docs/pages/api-reference/next-config-js/env
	 */
	NEXT_PUBLIC_MY_VALUE: process.env.NEXT_PUBLIC_MY_VALUE,
});

// Client Component
// import { env } from "@/utils/dotenv.public";
// console.log(env.NEXT_PUBLIC_MY_VALUE); // "xxx"
  • ※ フルパス(process.env.NEXT_PUBLIC_XXX)で記述しないと環境変数の値にアクセス出来ないので記述しています
  • ※ 場合によってはconstants.tsの様な定数を管理するファイルで事足りるかもしれません

Server Component

utils/dotenv.ts
import { g, validateEnv } from "@/libs/dotenv";
import { schema as publicSchema } from "@/utils/dotenv.public";

const schema = g.envObject({
	MY_SECRET: g.envString(),
});

export const env = validateEnv(publicSchema.merge(schema));

// Server Component
// import { env } from "@/utils/dotenv";
// console.log(env.NEXT_PUBLIC_MY_VALUE); // "xxx"
// console.log(env.MY_SECRET); // "***"
  • Client Componentの環境変数もアクセスできるのでスキーマをマージしました

さいごに

他にもスマートな書き方や、こんな書き方があったよなど、教えていただけますと幸いです。

Discussion