Tutorial
Building a Screenshot Service with Ruby on Rails and SnapAPI
April 2026 — 7 min read
Ruby on Rails is a full-stack web framework that powers thousands of production applications, from early-stage startups to large-scale platforms. Its convention-over-configuration philosophy and rich ecosystem of gems make adding new capabilities to an existing Rails application straightforward. In this tutorial, you will add screenshot and PDF generation to a Rails application using SnapAPI, implementing a controller action that validates URLs, calls SnapAPI, and returns binary responses with appropriate caching headers.
Adding a Screenshot Controller to Rails
Rails controllers handle HTTP requests and return responses. Generate a new controller with rails generate controller Screenshots show. This creates a controller at app/controllers/screenshots_controller.rb and a route in config/routes.rb. In the show action, extract the target URL from params[:url], validate it, call SnapAPI using Ruby's Net::HTTP or the Faraday gem, and send the binary response using Rails' send_data method with the appropriate MIME type.
URL Validation in Rails
Use Ruby's built-in URI.parse combined with explicit scheme checking to validate that the target URL is a valid HTTP or HTTPS URL before making any external requests. Wrap the validation in a rescue block that catches URI::InvalidURIError and returns a 400 response with a JSON error body using Rails' render json: helper. Add an explicit check that the parsed URI scheme is either http or https, rejecting file, data, javascript, and ftp schemes that URI.parse accepts but should not be passed to SnapAPI. This two-layer validation provides defense against server-side request forgery without requiring any additional gems.
Storing the SnapAPI Key in Rails Credentials
Rails credentials provide an encrypted storage mechanism for API keys and other secrets. Run rails credentials:edit to open the encrypted credentials file and add your SnapAPI key under a snapapi_key entry. Access it in your controller with Rails.application.credentials.snapapi_key. The credentials file is encrypted with your Rails master key, which you store separately from the credentials file -- in an environment variable in production and in a local file excluded from version control in development. This approach ensures your SnapAPI key is never committed to your Git repository in plaintext.
Calling SnapAPI with Faraday
Faraday is Ruby's most popular HTTP client gem, offering a middleware-based architecture and consistent interface across connection adapters. Add gem 'faraday' to your Gemfile and run bundle install. In your controller, create a Faraday connection to the SnapAPI base URL with a 30-second timeout and a connection adapter appropriate for your Rails server -- the default net_http adapter works correctly in most cases. Make a GET request to the SnapAPI screenshot endpoint with your key in the Authorization header and the target URL as a query parameter. Check the response status before calling send_data with the response body.
Background Jobs with Sidekiq
For Rails applications where screenshot generation is triggered by user actions but results are consumed asynchronously, Sidekiq provides reliable background job processing. Define an ApplicationJob that calls SnapAPI, stores the result in Active Storage, and triggers a notification to the requesting user when the screenshot is ready. Your controller action enqueues the Sidekiq job and returns a 202 response with the job ID immediately. The user's frontend polls a status endpoint until the screenshot is available, then fetches it from Active Storage. This architecture keeps your Rails controllers responsive even when SnapAPI calls take multiple seconds for complex page rendering.
Caching with Rails.cache
Rails provides a unified caching interface through Rails.cache that works with Redis, Memcached, and file-based backends depending on your environment configuration. Cache SnapAPI responses by URL hash to reduce API credit consumption and improve response times for repeated screenshot requests. In your controller, compute a cache key by hashing the target URL and screenshot parameters with Digest::MD5. Call Rails.cache.fetch(cache_key, expires_in: 1.hour) with a block that calls SnapAPI on a cache miss. The cached binary content is returned immediately on subsequent requests for the same URL within the cache window, with no SnapAPI call required.
Advanced Rails Screenshot Patterns
Beyond the basic controller action approach, several Rails patterns are worth implementing for production screenshot services that handle high request volumes, multiple output formats, and the specific operational requirements of a long-running Rails application.
Active Job Integration for Screenshot Generation
Active Job is Rails' built-in framework for background job processing, providing a consistent interface across Sidekiq, Resque, Delayed Job, and other job backends. Define a ScreenshotJob that inherits from ApplicationJob and implements a perform method that accepts a URL and screenshot parameters. Inside perform, call SnapAPI using Faraday, store the result in Active Storage using a Blob, and broadcast a notification to the requesting user using Action Cable or your notification service of choice. Your controller action enqueues the job with ScreenshotJob.perform_later(url, params) and returns a 202 response immediately. This pattern decouples request acceptance from screenshot processing, keeping your application responsive regardless of SnapAPI response times.
Active Storage for Screenshot Persistence
Rails Active Storage provides a unified interface for storing files in Amazon S3, Google Cloud Storage, Azure Blob Storage, and local disk. After receiving a screenshot from SnapAPI, create a StringIO object from the binary response data and attach it to an Active Storage Blob with the appropriate content type. Store the blob's signed ID or attachment URL in your database record for later retrieval. Active Storage handles file upload, CDN integration, and URL generation automatically, so your application serves screenshot images from the same cloud storage infrastructure it uses for other media assets without any additional configuration.
Turbo Stream Delivery for Screenshot Results
Hotwire Turbo Streams provide a Rails-native way to deliver screenshot results to the browser without full page reloads. When a screenshot job completes, broadcast a Turbo Stream from the job's after hook that replaces a placeholder element on the requesting page with the completed screenshot image. The user sees the screenshot appear in place without any manual polling or page refresh. This pattern works particularly well in Rails applications that have already adopted Hotwire for other real-time UI updates, since the screenshot delivery infrastructure reuses the same Action Cable connection and Turbo Stream conventions that the rest of the application uses for live updates.
Testing Rails Screenshot Controllers
Rails integration tests use ActionDispatch::Integration::Session to make requests to your controllers and assert on the response. For screenshot controller tests, stub the Faraday HTTP call using WebMock or the VCR gem to return a pre-stored PNG byte string without making real network requests. Your test assertions verify that valid URL parameters return a 200 response with image/png content type, that invalid URLs return a 400 with a JSON error body, and that missing authentication returns a 401. Use Rails' fixture files or FactoryBot to set up any database records your controller requires. Run these tests with RSpec or Minitest as part of your standard Rails test suite to catch screenshot controller regressions before deployment. Get started with the free tier at snapapi.pics.
Advanced Patterns for Screenshot APIs in Rails Applications
Once your basic SnapAPI integration is working in Rails, several advanced patterns can significantly increase the value you extract from the service. Background job integration is the most impactful. Rather than making synchronous SnapAPI calls in your controller actions — which would block the request thread and increase response times — offload screenshot requests to Sidekiq or GoodJob workers. The worker accepts the URL and a callback identifier, calls SnapAPI, stores the resulting image in Active Storage, and marks the screenshot record as complete. Your frontend polls or uses ActionCable to notify the user when the image is ready.
Caching is equally important for production Rails apps. If multiple users request screenshots of the same URL within a short window, you do not want to burn API quota on duplicate renders. A simple Redis cache keyed by URL and a TTL of 15 minutes eliminates redundant calls. Use Rails.cache.fetch with a block that calls SnapAPI — the cache hit path returns the stored image URL instantly, while the miss path calls the API, stores the result, and caches the URL for the next caller.
For multi-tenant SaaS applications built on Rails, per-tenant screenshot quotas are straightforward to implement. Store a monthly_screenshot_count on your Account model, increment it inside the Sidekiq worker after each successful SnapAPI call, and gate requests with a before_action that checks current_account.screenshot_quota_remaining. This lets you offer screenshot features as part of tiered plan pricing without over-consuming your SnapAPI allocation.
Testing screenshot integrations in Rails is best handled with VCR cassettes or WebMock stubs that return a fixture image. This keeps your CI pipeline fast and free of external dependencies while still exercising the full code path from controller to worker to Active Storage. SnapAPI returns standard PNG or JPEG binary responses, so your fixture files are straightforward to create from a single real API call recorded during development.