require 'uri'
require 'rack/utils'
module PaginationLinks
module_function
def build(request_url, page:, per_page:, total:)
last_page = [(total.to_f / per_page).ceil, 1].max
rels = {}
rels[:first] = 1
rels[:last] = last_page
rels[:prev] = page - 1 if page > 1
rels[:next] = page + 1 if page < last_page
rels.map { |rel, target| %(<#{page_url(request_url, target, per_page)}>; rel="#{rel}") }
.join(', ')
end
def page_url(request_url, page, per_page)
uri = URI.parse(request_url)
params = Rack::Utils.parse_nested_query(uri.query)
params['page'] = page
params['per_page'] = per_page
uri.query = Rack::Utils.build_nested_query(params)
uri.to_s
end
end
require 'sinatra/base'
require 'json'
require_relative 'pagination_links'
class API < Sinatra::Base
MAX_PER_PAGE = 100
get '/articles' do
content_type :json
page = [params.fetch('page', 1).to_i, 1].max
per_page = params.fetch('per_page', 20).to_i.clamp(1, MAX_PER_PAGE)
offset = (page - 1) * per_page
scope = Article.order(Sequel.desc(:published_at))
scope = scope.where(status: params['status']) if params['status']
total = scope.count
records = scope.limit(per_page).offset(offset).all
headers['Link'] = PaginationLinks.build(
request.url, page: page, per_page: per_page, total: total
)
headers['X-Total-Count'] = total.to_s
records.map(&:to_hash).to_json
end
end
require 'rack/test'
require 'rspec'
require_relative 'app'
RSpec.describe API do
include Rack::Test::Methods
def app
API
end
before { Article.dataset.delete; 45.times { Article.create(status: 'active') } }
it 'advertises a next link on a middle page' do
get '/articles', page: 2, per_page: 20
expect(last_response.headers['X-Total-Count']).to eq('45')
expect(last_response.headers['Link']).to include('rel="next"')
expect(last_response.headers['Link']).to include('rel="prev"')
end
it 'omits next on the final page' do
get '/articles', page: 3, per_page: 20
link = last_response.headers['Link']
expect(link).to_not include('rel="next"')
expect(link).to include('page=1>; rel="first"')
expect(JSON.parse(last_response.body).size).to eq(5)
end
end
This snippet shows how a Sinatra JSON endpoint exposes offset-based pagination while advertising navigation through the standard Link header defined by RFC 5988, so clients can follow next, prev, first, and last relations without guessing URL shapes. The core idea is that pagination metadata belongs in HTTP semantics rather than being buried in the response body, which keeps the payload a clean array and lets generic HTTP clients discover navigation automatically.
The LinkHeader helper module builds the header value. PaginationLinks.build computes which relations are valid given page, per_page, and total, then constructs absolute URLs by cloning the current request URI and rewriting only the page query parameter. Using URI and Rack::Utils.build_nested_query here avoids brittle string concatenation and correctly re-encodes existing filters, so a request like ?status=active&page=2 keeps its filter across every emitted link. The relations are assembled into the comma-separated <url>; rel="name" grammar the RFC mandates.
In app.rb, the /articles route treats page and per_page as untrusted input: clamp bounds per_page to a sane ceiling and both values are floored to 1, which prevents a client from requesting a million rows or a negative offset. The route counts the total once, fetches a single window with limit/offset via Sequel, and then delegates to the helper to set both the Link header and a convenience X-Total-Count header. Returning the collection as a bare JSON array keeps the contract simple, and the total is derived server-side so last always points somewhere real.
The trade-off with offset pagination is that deep pages get slower as the offset grows and rows shifting between requests can cause skips or duplicates; for large or write-heavy datasets a keyset/cursor approach is preferable, though the Link header contract shown here stays identical. pagination_spec.rb locks in the behavior with Rack::Test, asserting the header contains a rel="next" on a middle page and omits next on the final page. This pattern is worth reaching for whenever an API should be navigable by clients that already understand Link, such as anything modeled on the GitHub API.
Related snips
export interface RetryOptions {
retries: number;
baseMs: number;
maxMs: number;
signal?: AbortSignal;
onRetry?: (attempt: number, delay: number, err: unknown) => void;
Exponential backoff with jitter for retries
class PostsController < ApplicationController
def index
@posts = Post.includes(:author)
.order(created_at: :desc)
.page(params[:page])
.per(10)
Turbo Frames: infinite scroll with lazy-loading frame
require "csv"
class PeopleCsvStream
include Enumerable
HEADERS = %w[id full_name email signed_up_at plan].freeze
Resilient CSV Export as a Streamed Response
package deps
import (
"net"
"net/http"
"time"
HTTP client tuned for production: timeouts, transport, and connection reuse
package api
import (
"io"
"net/http"
"os"
Safe multipart uploads using temp files (bounded memory)
package deps
import (
"crypto/tls"
"crypto/x509"
"net/http"
mTLS client configuration with custom root CA pool
Share this code
Here's the card — post it anywhere.