Ruby
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Methods and Blocks
-
Section 4: Object-Oriented Ruby
-
Section 5: Metaprogramming
-
Section 6: Working with Data and Files
-
Section 7: Ruby Frameworks Overview
-
Section 8: Ecosystem and Testing
-
Section 9: Practical Projects
-
Section 10: Interview Practice
-
Section 11: Data Structures and Algorithms in Ruby
-
Section 12: More Practice Exercises
-
Section 13: Enumerable and Functional Style
-
Section 14: More OOP Practice
-
Section 15: More Testing
-
Section 16: Enumerable and Comparable Modules In Depth
-
Section 17: Ruby Standard Library: Core Utilities
-
Section 18: Ruby Standard Library: Data and Security
-
Section 19: Ruby Standard Library: CLI and Text
-
Section 20: Ruby Networking
-
Section 21: Ruby on Rails Deep Dive
-
Section 22: Ruby Metaprogramming Deep Dive
-
Section 23: Ruby Design Patterns
-
Section 24: Ruby Concurrency
-
Section 25: Ruby Testing Deep Dive
-
Section 26: Ruby Gems and Packaging
-
Section 27: Ruby Performance
-
Section 28: More Practice Exercises
-
Section 29: More Interview Practice
-
Section 30: Rails API Development
-
Section 31: Rails Authentication and Authorization
-
Section 32: Rails Testing Deep Dive
-
Section 33: Rails Performance
-
Section 34: Rails Deployment
-
Section 35: Sinatra and Lightweight Ruby Web Apps
-
Section 36: More Ruby Language Deep Dive
-
Section 37: Ruby 3.x Modern Features
-
Section 38: More Data Structures in Ruby
-
Section 39: More Practical Projects
-
Section 40: Ruby Ecosystem Tools
-
Section 41: More Practice and Review
-
Section 42: Final Practice and Mastery
-
Section 43: Ruby Interview Deep Dive
-
Section 44: Ruby Background Processing Deep Dive
-
Section 45: Ruby GraphQL
-
Section 46: Ruby Object Model Deep Dive
-
Section 47: Ruby Hanami Framework Overview
-
Section 48: Ruby gRPC and Protocol Buffers
-
Section 49: Ruby Data Processing
-
Section 50: Ruby Search Integration
-
Section 51: Ruby File Upload and Media
-
Section 52: Ruby Email and Notifications
-
Section 53: Ruby Admin Panels
-
Section 54: Ruby Feature Flags and Experimentation
-
Section 55: Ruby Monitoring and Observability
-
Section 56: Ruby Docker and Deployment Deep Dive
-
Section 57: Ruby Security Deep Dive
-
Section 58: More Advanced Metaprogramming
-
Section 59: More Final Projects
141: Environment-Specific Configuration
In the early days of a project, it's tempting to just hardcode your API keys or database URLs. It works, it's fast, and you can see exactly where the data is coming from. But as soon as you have a "development" machine and a "production" server, that approach becomes a liability. You don't want to accidentally charge a real credit card while testing a feature, and you definitely don't want your production secrets committed to GitHub.
The Danger of Hardcoded Credentials
Let's build a simple PaymentClient. I want this client to talk to a third-party payment gateway. If I start like this, I'm already making a mistake:
class PaymentClient
API_KEY = "sk_test_4eC39HqLyjWDarjtT1zdp7dc" # Oops, hardcoded!
BASE_URL = "https://api.sandbox.paymentgateway.com"
def process_payment(amount)
puts "Connecting to #{BASE_URL} with key #{API_KEY}..."
# Logic to send the request would go here
end
end
client = PaymentClient.new
client.process_payment(100)
This is fine for ten minutes. But what happens when we go live? I'd have to manually change these strings, commit them, and deploy. If I forget? I'm running production traffic against a sandbox environment. Not ideal.
Switching to Environment Variables
The standard way to handle this in Ruby is via ENV. It's a built-in Ruby hash that gives you access to your shell's environment variables. It keeps secrets out of the source code entirely.
class PaymentClient
def initialize
@api_key = ENV['PAYMENT_API_KEY']
@base_url = ENV['PAYMENT_BASE_URL']
end
def process_payment(amount)
raise "Missing configuration!" if @api_key.nil?
puts "Connecting to #{@base_url} with key #{@api_key}..."
end
end
Now, I can run this in my terminal as PAYMENT_API_KEY=test_key PAYMENT_BASE_URL=https://sandbox.api.com ruby payment_client.rb. It's cleaner, but managing ten different variables in a shell command is a nightmare. We need a way to group these by environment.
Structuring Configuration with YAML
I usually prefer a config.yml file for non-sensitive defaults, combined with environment variables for the actual secrets. It gives you a clear map of what your environments look like. I'll create a file that looks like this:
# config.yml
development:
base_url: "https://api.sandbox.paymentgateway.com"
timeout: 30
production:
base_url: "https://api.paymentgateway.com"
timeout: 10
Now, let's update the client to load this file based on an APP_ENV variable.
require 'yaml'
class PaymentClient
def initialize
# Load the YAML file
config_data = YAML.load_file('config.yml')
# Determine which environment we are in
env = ENV['APP_ENV'] || 'development'
# Merge YAML defaults with a sensitive ENV key
@config = config_data[env]
@api_key = ENV['PAYMENT_API_KEY']
end
def process_payment(amount)
puts "Env: #{ENV['APP_ENV'] || 'development'}"
puts "Connecting to #{@config['base_url']} with key #{@api_key}..."
end
end
The "Defaulting to Production" Trap
Here is where I messed up on a project a couple of years ago. I thought it was "safer" to default to production if the environment wasn't specified, thinking that would prevent the app from failing in the wild. I wrote something like env = ENV['APP_ENV'] || 'production'.
That is a catastrophic mistake. I spent a whole afternoon debugging why my local development machine was trying to hit the production database. Always default to the safest, most restricted environment—usually development or test.
If you look at my code above, I used ENV['APP_ENV'] || 'development'. This ensures that if I'm just running the script locally without any fancy setup, I can't possibly accidentally wipe out production data.
By combining a YAML file for structural configuration and ENV` for secrets, you get a system that is easy to reason about and safe to commit to version control (provided you add config.yml to your .gitignore if it contains anything sensitive, or keep only non-secrets there).
📋 Practical Task
Build a Multi-Environment Weather Service Configurator
Your task is to create a WeatherService class that handles configuration for three environments: development, test, and production.
- Create a
weather_config.ymlfile containing aapi_endpointand acache_ttl(time-to-live) for each of the three environments. - Implement the
WeatherServiceclass so that it:- Loads the
weather_config.ymlfile. - Determines the current environment using
ENV['WEATHER_ENV'], defaulting to'development'. - Retrieves an
API_KEYfromENV['WEATHER_API_KEY'].
- Loads the
- Add a method
connection_detailsthat prints the endpoint, the TTL, and the API key being used. - Test your implementation by running the script twice in your terminal: once with
WEATHER_ENV=productionand once with no environment variables set.
There are no comments for now.