Introduction to Environment Variables
Imagine you're building a house. You wouldn't want to write your house's security code directly on the front door, would you? Similarly, in software development, we don't want to expose sensitive information like API keys or database credentials directly in our code. This is where environment variables and dotenv come into play.
Think of environment variables as a secure vault where you store your valuable information, and dotenv as the organized filing system for that vault. Just as a bank keeps its customers' valuables in separate safety deposit boxes, dotenv helps us manage different sets of environment variables for different environments (development, testing, production).
Why We Need dotenv
Let's explore a real-world scenario: You're developing a weather application that needs to connect to a weather API and store historical data in a database. Your application needs:
- An API key for the weather service
- Database connection credentials
- Server configuration settings
Without dotenv, you might be tempted to write:
const database = new Database('mypassword123');
const apiKey = 'ab12cd34ef56';
This approach creates several problems:
- Security Risk: If you push this code to GitHub, anyone can see your credentials
- Environment Inflexibility: The same credentials are used in development and production
- Collaboration Challenges: Team members need different configurations
Setting Up dotenv
Let's walk through setting up dotenv in your project:
# Install dotenv
npm install dotenv
# Create a .env file in your project root
touch .env
# Create a .env.example file for documentation
touch .env.example
Your .env file might look like this:
DB_NAME=weather_app
DB_USER=admin
DB_PASSWORD=secretpassword
WEATHER_API_KEY=ab12cd34ef56
And your .env.example:
DB_NAME=your_database_name
DB_USER=your_username
DB_PASSWORD=your_password
WEATHER_API_KEY=your_api_key
Integrating with Sequelize and SQLite3
Let's implement a practical example using Sequelize with SQLite3. First, install the required packages:
npm install sequelize sqlite3
Create a database configuration file (config.js):
require('dotenv').config();
module.exports = {
development: {
dialect: 'sqlite',
storage: process.env.DB_PATH || './database.sqlite',
logging: console.log
},
test: {
dialect: 'sqlite',
storage: ':memory:',
logging: false
},
production: {
dialect: 'sqlite',
storage: process.env.DB_PATH,
logging: false
}
};
Create your database connection (database.js):
const { Sequelize } = require('sequelize');
const config = require('./config')[process.env.NODE_ENV || 'development'];
const sequelize = new Sequelize(config);
module.exports = sequelize;
Working with API Keys
Let's create a weather service that uses our environment variables:
// weatherService.js
require('dotenv').config();
class WeatherService {
constructor() {
this.apiKey = process.env.WEATHER_API_KEY;
if (!this.apiKey) {
throw new Error('Weather API key not found in environment variables');
}
}
async getWeather(city) {
const response = await fetch(
`https://api.weatherservice.com/v1/current?key=${this.apiKey}&q=${city}`
);
return response.json();
}
}
module.exports = new WeatherService();
Best Practices and Common Pitfalls
Here are some important practices to follow:
- Always add .env to your .gitignore file
- Never commit actual credentials in .env.example
- Validate environment variables on application startup
- Use different .env files for different environments (.env.development, .env.production)
Here's a validation example:
// validateEnv.js
function validateEnv() {
const required = [
'DB_NAME',
'DB_USER',
'DB_PASSWORD',
'WEATHER_API_KEY'
];
const missing = required.filter(key => !process.env[key]);
if (missing.length > 0) {
throw new Error(
`Missing required environment variables: ${missing.join(', ')}`
);
}
}
module.exports = validateEnv;
Practical Exercise
Let's build a small weather logging application that brings everything together:
// index.js
require('dotenv').config();
const express = require('express');
const sequelize = require('./database');
const WeatherService = require('./weatherService');
const validateEnv = require('./validateEnv');
// Validate environment variables before starting
validateEnv();
const app = express();
// Define Weather model
const Weather = sequelize.define('Weather', {
city: String,
temperature: Number,
conditions: String,
timestamp: Date
});
app.get('/weather/:city', async (req, res) => {
try {
const weatherData = await WeatherService.getWeather(req.params.city);
// Log to database
await Weather.create({
city: req.params.city,
temperature: weatherData.temperature,
conditions: weatherData.conditions,
timestamp: new Date()
});
res.json(weatherData);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Sync database and start server
sequelize.sync().then(() => {
app.listen(process.env.PORT || 3000, () => {
console.log('Weather logging service started');
});
});
Further Topics to Explore
To deepen your understanding, consider exploring:
- Environment-specific configuration management
- Secret rotation and management in production
- Integration with cloud services (AWS, Google Cloud, Azure)
- Docker environment variable management
- Continuous Integration/Deployment with environment variables