Short links are everywhere: on posters, in social posts, behind QR codes. The shortening is the easy part. The useful part is knowing whether anyone used them, and that is usually where tools start tracking people far more than they need to.
rails-url-shortener is a small, production-shaped service built with Rails 8.1. It creates short links, redirects visitors, and answers the questions that matter (how many clicks, from where, how many different people today) while deliberately keeping as little personal data as possible.
01What it does
- Create a link, optionally with your own name for it (
/doctrine) and an expiry date. - Redirect visitors to the original address, and answer “gone” once a link has expired.
- Report usage: total clicks, unique visitors today, clicks per day for the last week, and the top five places visitors came from.
# create curl -s -X POST localhost:3000/api/v1/links -H 'Content-Type: application/json' \ -d '{"url":"https://rubyonrails.org/doctrine","slug":"doctrine"}' → {"slug":"doctrine","short_url":"http://localhost:3000/doctrine",...} # follow (302) — every visit is recorded curl -i localhost:3000/doctrine # stats curl -s localhost:3000/api/v1/links/doctrine/stats → {"total_clicks":1,"unique_visitors_today":1,"last_7_days":[...],"top_referrers":[...]}
02What happens on a click
The interesting part is the visitor id. To count unique visitors, you need to recognise the same person twice; the lazy way is to store their IP address. Instead, each click stores a SHA-256 hash of a server secret, today's date, the IP and the browser's user agent. Two visits from the same IP and browser on the same day produce the same hash, so they count once. Tomorrow the date changes, and so does the hash.
Enough to count visitors. Not enough to follow them from day to day.
03Decisions I made, and why
- 302, not 301. A permanent redirect lets browsers skip the server on repeat visits, which would make click counts meaningless. A temporary one keeps every visit visible.
- Only web addresses. Links must be
httporhttpswith a host.javascript:anddata:addresses are rejected, so a short link can never become a way to run code in someone's browser. - Short, unguessable, unique slugs. Seven random letters and digits give about 3.5 trillion combinations, with a retry on the rare collision. Custom names are validated, checked against reserved words like
api, and kept unique regardless of letter case. - Built-in abuse protection. Creating links is limited to 30 per minute per IP, using Rails' own rate limiting.
- Errors you can program against. Every error, including “not found”, comes back as JSON, with field-level messages when validation fails.
04Production-shaped
It is small, but built the way a real service would be. It ships with a production Dockerfile, and every push runs the linter (RuboCop), a static security scan (Brakeman) and the test suite: 18 tests and 95 assertions covering the models and the HTTP endpoints. It uses SQLite by default, and switching databases is a one-line configuration change because nothing in the code depends on SQLite.
05Known limits
The rate-limit counter lives in each server process's memory, so with several processes it should be pointed at a shared store. There is no web interface; it is an API, meant to sit behind an app, a bot or a script.
Try it or read the code
Open source under the MIT licence. Needs Ruby 3.3:
git clone https://github.com/Rodzman/rails-url-shortener cd rails-url-shortener && bin/setup --skip-server && bin/rails server