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
247: Building a Time-Tracking Application API
You might be tempted to think that the most efficient way to build a time-tracking API is to simply store a duration column (perhaps as an integer of seconds) in your database. It seems intuitive: when the user hits "Stop," you calculate the difference between the start and end times and save that single number. It feels clean, and it makes your SUM() queries in SQL incredibly fast.
The Trap of the Duration Column
Here is why that approach will bite you. Imagine your user starts a timer at 9:00 AM and stops it at 10:30 AM. You store 5400 seconds. Everything looks great until the client asks for a "Daily Timeline" view. Suddenly, you need to show that the user was active between 9:00 and 10:30. If you only stored the duration, that temporal data is gone forever. You can't reconstruct the timeline; you've effectively deleted the "when" and only kept the "how long."
Tracking Intervals, Not Sums
The correct way to handle this is to treat time as an interval. You need two timestamps: started_at and stopped_at. This preserves the integrity of the data. If you need the duration, you calculate it on the fly or use a virtual attribute. I've found that relying on the database to hold the "truth" of the timestamps and letting Ruby handle the duration logic keeps the API flexible.
class TimeEntry < ApplicationRecord
# We assume started_at and stopped_at are datetime columns
def duration
return 0 unless started_at
# If the timer is still running, we calculate duration up to now
end_time = stopped_at || Time.current
(end_time - started_at).to_i # Returns duration in seconds
end
def active?
stopped_at.nil?
end
end
Handling the 'Still Running' Timer
In a real-world API, you'll run into the "zombie timer" problem—where a user starts a timer and then closes their browser or their laptop dies. If your API endpoint for GET /entries simply subtracts stopped_at from started_at, any active timer will return a nil or an error because stopped_at is empty.
I usually handle this by implementing a helper method that treats a nil end time as "right now." This ensures that your API always returns a current duration for active tasks, which makes the frontend feel responsive and live.
# In your controller or a serializer
def render_time_entry(entry)
{
id: entry.id,
project_id: entry.project_id,
started_at: entry.started_at,
stopped_at: entry.stopped_at,
duration_seconds: entry.duration, # Uses our model method from above
status: entry.active? ? 'running' : 'completed'
}
end
Structuring the API Endpoints for State Changes
Avoid creating generic "update" endpoints for starting and stopping timers. While PATCH /time_entries/1 works, it's imprecise. I prefer explicit action endpoints. It makes your logs easier to read and your business logic easier to isolate. Instead of sending a payload with a timestamp, just hit a toggle.
POST /time_entries: Creates a new entry and setsstarted_atto the current time.POST /time_entries/:id/stop: Setsstopped_atto the current time.
This approach removes the risk of the client sending a skewed timestamp from their local machine, which is a nightmare to debug when users are in different time zones. Always trust the server's clock for audit-trail data.
📋 Practical Task
Implement a Project Totalizer Endpoint
You have a Project model that has_many :time_entries. Your goal is to add a custom action to your API that calculates the total time spent on a project, including the time elapsed on any entries that are currently still running.
Requirements:
- Create a route for
GET /projects/:id/total_time. - In the controller, sum the durations of all associated
TimeEntryrecords. - Ensure that entries where
stopped_atisnilare calculated up to the current moment (Time.current). - The response should be a JSON object containing the
project_idand thetotal_seconds.
There are no comments for now.