ruby 73 lines · 3 tabs

Conditional GET in Sinatra with ETag and Last-Modified for Cacheable JSON Endpoints

Shared by codesnips Aug 2026
3 tabs
require 'digest'
require 'json'

module CacheHelpers
  def cache_control_public(max_age = 60)
    cache_control :public, :must_revalidate, max_age: max_age
  end

  def stable_etag(payload)
    Digest::SHA256.hexdigest(payload)
  end

  # last_modified: a Time used as a coarse validator
  # &block: lazily builds the response body (only run on a cache miss)
  def serve_cacheable(last_modified:, max_age: 60)
    cache_control_public(max_age)

    # Coarse revalidation first; halts 304 if If-Modified-Since matches.
    last_modified(last_modified) if last_modified

    body = yield
    content_type :json

    # Precise revalidation; halts 304 if If-None-Match matches.
    etag stable_etag(body)
    body
  end
end
3 files · ruby Explain with highlit

This snippet shows how a Sinatra API can avoid re-serializing and re-sending expensive route responses by implementing HTTP conditional GET around ETag and Last-Modified. The core idea is that a client already holding a cached copy sends If-None-Match and If-Modified-Since; when the resource is unchanged, the server can short-circuit with a 304 Not Modified and an empty body, saving bandwidth and CPU while keeping correctness.

The CacheHelpers module centralizes the logic. cache_control_public sets a public, revalidatable Cache-Control header so intermediaries may store the response but must revalidate once stale. serve_cacheable is the workhorse: it accepts a last_modified timestamp and a block that lazily builds the body. It computes a strong validator with stable_etag, then calls Sinatra's built-in etag and last_modified helpers, which internally compare the request's If-None-Match/If-Modified-Since and halt 304 before the block ever runs. That laziness matters — the whole point is that on a cache hit the expensive JSON.generate and any database work behind it are skipped.

stable_etag deliberately hashes the serialized payload with Digest::SHA256 so the validator changes only when the bytes change; it is derived from content rather than a random token so the same representation always yields the same tag. Note the order: last_modified is set first as a coarse validator, and etag as the precise one, matching how Sinatra evaluates conditional requests.

In Reports API, the Sinatra::Base app mixes in the helpers and uses them on /reports/:id. The route fetches a lightweight updated_at for validation, passes it to serve_cacheable, and only inside the block performs the costly report.render_summary aggregation. Because the block is skipped on 304, a repeated request from a warm client costs almost nothing.

The trade-offs are worth understanding: strong ETags require reading enough to serialize, so pairing them with Last-Modified lets clients revalidate cheaply on the timestamp alone. Last-Modified has one-second granularity, which is why the ETag is kept as the authoritative validator. This pattern fits read-heavy, slowly-changing JSON resources; it is a poor fit for personalized or rapidly-mutating data, where Cache-Control: private, no-store is safer.


Related snips

Share this code

Here's the card — post it anywhere.

Conditional GET in Sinatra with ETag and Last-Modified for Cacheable JSON Endpoints — share card
Link copied