add-env-variable
Add a new environment variable to the application. Use when adding configuration for external services, feature flags, or application settings. Triggers on "add env", "environment variable", "config variable".
$ 安裝
git clone https://github.com/madooei/backend-template /tmp/backend-template && cp -r /tmp/backend-template/.claude/skills/add-env-variable ~/.claude/skills/backend-template// tip: Run this command in your terminal to install the skill
name: add-env-variable description: Add a new environment variable to the application. Use when adding configuration for external services, feature flags, or application settings. Triggers on "add env", "environment variable", "config variable".
Add Environment Variable
Adds a new environment variable with Zod validation. All environment variables must be defined in src/env.ts and documented in .env.example.
Quick Reference
Files to modify:
src/env.ts- Add to schema and mapping.env.example- Document the variabletests/env.test.ts- Add validation tests
Instructions
Step 1: Add to Schema in src/env.ts
Add the variable to the envSchema object:
const envSchema = z.object({
// ... existing variables ...
// Your new variable (with comment explaining purpose)
NEW_VARIABLE: z.string(), // Required string
// OR
NEW_VARIABLE: z.string().optional(), // Optional string
// OR
NEW_VARIABLE: z.string().default("default-value"), // With default
// OR
NEW_VARIABLE: z.coerce.number().default(3000), // Number with coercion
// OR
NEW_VARIABLE: z.string().url(), // URL validation
});
Step 2: Add to Mapping Object
Add the variable to mappedEnv using the getEnv() helper (which reads prefixed variables):
const mappedEnv = {
// ... existing mappings ...
NEW_VARIABLE: getEnv("NEW_VARIABLE"),
};
Step 3: Document in .env.example
Add the variable with a descriptive comment, using the prefix (default: BT_):
# Description of what this variable is for
BT_NEW_VARIABLE=example-value
Note: The prefix is defined in
src/env.tsasconst PREFIX = "BT". Change this when creating a new service from the template.
Step 4: Add Tests in tests/env.test.ts
Add test cases for the new variable:
it("accepts valid NEW_VARIABLE", () => {
const parsed = envSchema.parse({ NEW_VARIABLE: "valid-value" });
expect(parsed.NEW_VARIABLE).toBe("valid-value");
});
it("defaults NEW_VARIABLE if missing", () => {
const parsed = envSchema.parse({});
expect(parsed.NEW_VARIABLE).toBe("default-value");
});
// OR for optional
it("accepts missing NEW_VARIABLE", () => {
const parsed = envSchema.parse({});
expect(parsed.NEW_VARIABLE).toBeUndefined();
});
// OR for required
it("rejects missing NEW_VARIABLE", () => {
expect(() => envSchema.parse({})).toThrow();
});
Common Patterns
All examples use the getEnv() helper and BT_ prefix:
Required String
// Schema
MY_API_KEY: z.string(),
// Mapping
MY_API_KEY: getEnv("MY_API_KEY"),
// .env.example
BT_MY_API_KEY=your-api-key-here
Optional String
// Schema
OPTIONAL_FEATURE: z.string().optional(),
// Mapping
OPTIONAL_FEATURE: getEnv("OPTIONAL_FEATURE"),
// .env.example
# Optional: Enable feature X
# BT_OPTIONAL_FEATURE=enabled
String with Default
// Schema
LOG_LEVEL: z.string().default("info"),
// Mapping
LOG_LEVEL: getEnv("LOG_LEVEL"),
// .env.example
BT_LOG_LEVEL=info
Number with Coercion
// Schema
RATE_LIMIT: z.coerce.number().default(100),
// Mapping
RATE_LIMIT: getEnv("RATE_LIMIT"),
// .env.example
BT_RATE_LIMIT=100
URL Validation
// Schema
WEBHOOK_URL: z.string().url().optional(),
// Mapping
WEBHOOK_URL: getEnv("WEBHOOK_URL"),
// .env.example
# Webhook endpoint for notifications
BT_WEBHOOK_URL=https://example.com/webhook
Enum Values
// Schema
NODE_ENV: z.enum(["development", "test", "production"]).default("development"),
// Mapping
NODE_ENV: getEnv("NODE_ENV"),
// .env.example
BT_NODE_ENV=development
Boolean (as string)
// Schema
ENABLE_FEATURE: z.string().transform(v => v === "true").default("false"),
// Mapping
ENABLE_FEATURE: getEnv("ENABLE_FEATURE"),
// .env.example
BT_ENABLE_FEATURE=false
Usage in Code
Always import from @/env, never use process.env directly:
import { env } from "@/env";
// Correct
const apiKey = env.MY_API_KEY;
// Wrong - bypasses validation
const apiKey = process.env.MY_API_KEY;
Full Example: Adding Email Service Config
1. Update src/env.ts
const envSchema = z.object({
// ... existing ...
// Email service configuration
EMAIL_API_KEY: z.string().optional(),
EMAIL_FROM_ADDRESS: z.string().email().optional(),
EMAIL_PROVIDER: z.enum(["sendgrid", "mailgun"]).default("sendgrid"),
});
const mappedEnv = {
// ... existing ...
EMAIL_API_KEY: getEnv("EMAIL_API_KEY"),
EMAIL_FROM_ADDRESS: getEnv("EMAIL_FROM_ADDRESS"),
EMAIL_PROVIDER: getEnv("EMAIL_PROVIDER"),
};
2. Update .env.example
# Email service configuration
BT_EMAIL_API_KEY=your-email-api-key
BT_EMAIL_FROM_ADDRESS=noreply@example.com
BT_EMAIL_PROVIDER=sendgrid
3. Update tests/env.test.ts
it("accepts valid email configuration", () => {
const parsed = envSchema.parse({
EMAIL_API_KEY: "test-key",
EMAIL_FROM_ADDRESS: "test@example.com",
EMAIL_PROVIDER: "mailgun",
});
expect(parsed.EMAIL_API_KEY).toBe("test-key");
expect(parsed.EMAIL_FROM_ADDRESS).toBe("test@example.com");
expect(parsed.EMAIL_PROVIDER).toBe("mailgun");
});
it("defaults EMAIL_PROVIDER to sendgrid", () => {
const parsed = envSchema.parse({});
expect(parsed.EMAIL_PROVIDER).toBe("sendgrid");
});
it("rejects invalid EMAIL_FROM_ADDRESS", () => {
expect(() =>
envSchema.parse({ EMAIL_FROM_ADDRESS: "not-an-email" }),
).toThrow();
});
it("rejects invalid EMAIL_PROVIDER", () => {
expect(() => envSchema.parse({ EMAIL_PROVIDER: "invalid" })).toThrow();
});
What NOT to Do
- Do NOT use
process.envdirectly in application code - Do NOT forget to add the mapping in
mappedEnv - Do NOT skip documenting in
.env.example - Do NOT skip adding tests for validation rules
- Do NOT store secrets in
.env.example(use placeholder values)
See Also
create-utility-service- Services that use environment configtest-schema- Testing Zod schemas (similar patterns)
Repository
