Environment Variables and dotenv
Environment variables are how you pass configuration to a running process without hardcoding values in source code. They’re the standard way to handle database URLs, API keys, JWT secrets, and port numbers across different environments (local, staging, production).
Why environment variables?
Section titled “Why environment variables?”Imagine hardcoding your JWT secret:
// ❌ Never do thisconst JWT_SECRET = 'my-super-secret-key-12345'This gets committed to git, pushed to GitHub, and anyone who sees the repo can forge JWTs for your API. Environment variables keep secrets out of source code.
Reading environment variables
Section titled “Reading environment variables”Node.js exposes environment variables via process.env:
const port = process.env.PORT // '3000' (string)const secret = process.env.JWT_SECRET // 'abc...' or undefinedconst env = process.env.NODE_ENV // 'development' | 'production' | undefinedAll values are strings. Parse numbers explicitly:
const port = parseInt(process.env.PORT || '3000', 10)dotenv
Section titled “dotenv”In production (Railway, Vercel, etc.), environment variables are set in the hosting platform’s dashboard. Locally, you need another way to set them without typing them on every command.
dotenv loads a .env file into process.env:
npm install dotenvCreate .env in the project root:
PORT=3000JWT_SECRET=change-this-to-a-long-random-string-in-productionDATABASE_PATH=./bulletin.dbNODE_ENV=developmentLoad it at the very start of your application:
// src/index.ts — first line before any other imports that use process.envimport 'dotenv/config'
// Now process.env.PORT, process.env.JWT_SECRET, etc. are available.gitignore
Section titled “.gitignore”Never commit .env to git. It contains secrets. Add it to .gitignore:
node_modules/dist/.env*.db.env.example
Section titled “.env.example”Commit a .env.example file that lists required variables without their values:
PORT=JWT_SECRET=DATABASE_PATH=NODE_ENV=This documents what variables the application needs without exposing actual values. New developers copy this to .env and fill in their own values.
Validating required variables
Section titled “Validating required variables”Don’t let the server start with missing configuration:
import 'dotenv/config'
function required(name: string): string { const value = process.env[name] if (!value) throw new Error(`Environment variable ${name} is required`) return value}
export const config = { port: parseInt(process.env.PORT || '3000', 10), jwtSecret: required('JWT_SECRET'), dbPath: process.env.DATABASE_PATH || './bulletin.db', nodeEnv: process.env.NODE_ENV || 'development',}Import config instead of accessing process.env directly throughout the codebase. This centralizes validation and provides TypeScript types.
Setting variables on Railway
Section titled “Setting variables on Railway”When you deploy the Bulletin API to Railway:
- Go to your Railway project → Variables
- Add each variable:
JWT_SECRET,PORT,NODE_ENV=production - Railway injects them into
process.envat runtime — no.envfile needed
Exercise
Section titled “Exercise”- Create a
.envfile in your project withPORT=3000,JWT_SECRET=dev-secret-change-in-prod, andNODE_ENV=development. - Create
src/config.tswith the validation pattern above. - Import
configinsrc/index.tsand logconfig.port. - Delete
JWT_SECRETfrom.envand observe the error on startup. - Create
.env.examplelisting all required variables without values.
- Environment variables keep secrets and config out of source code.
process.env.NAMEreads a variable — value is always a string.dotenv(andimport 'dotenv/config') loads.envintoprocess.envfor local development.- Never commit
.env; always commit.env.example. - Centralize env validation in a
config.tsfile for clean, typed access across the codebase.