URL Shortener
Abstract
A URL shortener compresses a long URL like https://example.com/very/long/path?query=1&foo=2https://example.com/very/long/path?query=1&foo=2 into something like https://tinyurl.com/abc123https://tinyurl.com/abc123. The short link redirects to the original when clicked. In this tutorial you will start by using the pyshortenerspyshorteners library to talk to existing services (TinyURL, Bitly), then go deeper and build your own shortener with Flask, SQLite, and a base62 encoder — the same architecture every commercial shortener uses.
You will learn:
- How third-party APIs wrap real services.
- How HTTP redirects work.
- How short codes are generated and looked up.
- Why custom shorteners need careful URL validation.
- How to add analytics (click counts, referrers) and rate limiting.
Prerequisites
- Python 3.6 or above.
- A code editor or IDE.
- An internet connection (for the third-party portion).
- Comfort with
pip installpip installand basic web concepts.
Install Dependencies
pip install pyshortenerspip install pyshortenersFor the “build your own” section later:
pip install flaskpip install flaskPart 1 — Use a Third-Party Service
Getting started
- Create a folder named
url-shortenerurl-shortener. - Inside it, create
urlshorter.pyurlshorter.py.
Write the code
URL Shortener
Source# URL Shorter
import pyshorteners # pip install pyshorteners
url = input("Enter URL: ")
# TinyURL
shortener = pyshorteners.Shortener()
shortURL = shortener.tinyurl.short(url)
# Bitly
# bitlyShortener = pyshorteners.Shortener(api_key='01b6c587cskek4kdfijsjce4cf27ce2') # Change API Key with your own API Key
# shortURL = bitlyShortener.bitly.short(url) # bitly
print("Short URL: ", shortURL)# URL Shorter
import pyshorteners # pip install pyshorteners
url = input("Enter URL: ")
# TinyURL
shortener = pyshorteners.Shortener()
shortURL = shortener.tinyurl.short(url)
# Bitly
# bitlyShortener = pyshorteners.Shortener(api_key='01b6c587cskek4kdfijsjce4cf27ce2') # Change API Key with your own API Key
# shortURL = bitlyShortener.bitly.short(url) # bitly
print("Short URL: ", shortURL)Run it
C:\Users\Your Name\url-shortener> python urlshorter.py
Enter URL: https://www.example.com/very/long/url/with/many/parameters?param1=value1¶m2=value2
Short URL: https://tinyurl.com/abc123defC:\Users\Your Name\url-shortener> python urlshorter.py
Enter URL: https://www.example.com/very/long/url/with/many/parameters?param1=value1¶m2=value2
Short URL: https://tinyurl.com/abc123defStep-by-Step Explanation
1. Import the library
import pyshortenersimport pyshorteners2. Validate and shorten
url = input("Enter URL: ").strip()
if not (url.startswith("http://") or url.startswith("https://")):
raise SystemExit("URL must start with http:// or https://")
shortener = pyshorteners.Shortener()
short_url = shortener.tinyurl.short(url)
print("Short URL:", short_url)url = input("Enter URL: ").strip()
if not (url.startswith("http://") or url.startswith("https://")):
raise SystemExit("URL must start with http:// or https://")
shortener = pyshorteners.Shortener()
short_url = shortener.tinyurl.short(url)
print("Short URL:", short_url)- TinyURL needs no API key.
- Bitly does — set up an API token and use
pyshorteners.Shortener(api_key="...")pyshorteners.Shortener(api_key="...").
3. Handle network errors
from pyshorteners.exceptions import ShorteningErrorException, ExpandingErrorException
import requests
try:
short_url = shortener.tinyurl.short(url)
except (ShorteningErrorException, requests.RequestException) as e:
print("Could not shorten:", e)from pyshorteners.exceptions import ShorteningErrorException, ExpandingErrorException
import requests
try:
short_url = shortener.tinyurl.short(url)
except (ShorteningErrorException, requests.RequestException) as e:
print("Could not shorten:", e)What pyshortenerspyshorteners Supports
| Service | Needs API key? |
|---|---|
| TinyURL | No |
| Is.gd | No |
| V.gd | No |
| Clck.ru | No |
| Chilp.it | No |
| Bitly | Yes |
| QR.net | No |
| Owly | Yes |
| Cuttly | Yes |
Switching service is just changing the attribute: shortener.isgd.short(url)shortener.isgd.short(url), shortener.bitly.short(url)shortener.bitly.short(url), etc.
Part 2 — Build Your Own Shortener
Using a service is fine. Building one yourself is far more educational and gives you full control.
How a real shortener works
- User submits a long URL.
- Server stores
{ short_code → long_url }{ short_code → long_url }in a database. - Server returns
https://short.example.com/<short_code>https://short.example.com/<short_code>. - When that short URL is visited, the server looks up the code and returns an HTTP 302 redirect to the long URL.
The short code is usually a base62 (0-9a-zA-Z0-9a-zA-Z) encoding of an auto-incrementing ID — 6 characters yields ~57 billion possible URLs.
Code
from flask import Flask, request, redirect, jsonify
import sqlite3, string, secrets
app = Flask(__name__)
ALPHABET = string.ascii_letters + string.digits # 62 characters
def init_db():
with sqlite3.connect("urls.db") as db:
db.execute("""CREATE TABLE IF NOT EXISTS urls (
code TEXT PRIMARY KEY,
url TEXT NOT NULL,
clicks INTEGER DEFAULT 0
)""")
def new_code(length=6):
return "".join(secrets.choice(ALPHABET) for _ in range(length))
@app.post("/shorten")
def shorten():
long_url = request.json["url"].strip()
if not long_url.startswith(("http://", "https://")):
return jsonify(error="URL must include http:// or https://"), 400
with sqlite3.connect("urls.db") as db:
# generate codes until one is unique
while True:
code = new_code()
try:
db.execute("INSERT INTO urls(code, url) VALUES(?, ?)",
(code, long_url))
break
except sqlite3.IntegrityError:
continue
return jsonify(short=f"{request.host_url}{code}")
@app.get("/<code>")
def follow(code):
with sqlite3.connect("urls.db") as db:
row = db.execute("SELECT url FROM urls WHERE code=?", (code,)).fetchone()
if not row:
return "Not found", 404
db.execute("UPDATE urls SET clicks = clicks + 1 WHERE code=?", (code,))
return redirect(row[0], code=302)
if __name__ == "__main__":
init_db()
app.run(debug=True)from flask import Flask, request, redirect, jsonify
import sqlite3, string, secrets
app = Flask(__name__)
ALPHABET = string.ascii_letters + string.digits # 62 characters
def init_db():
with sqlite3.connect("urls.db") as db:
db.execute("""CREATE TABLE IF NOT EXISTS urls (
code TEXT PRIMARY KEY,
url TEXT NOT NULL,
clicks INTEGER DEFAULT 0
)""")
def new_code(length=6):
return "".join(secrets.choice(ALPHABET) for _ in range(length))
@app.post("/shorten")
def shorten():
long_url = request.json["url"].strip()
if not long_url.startswith(("http://", "https://")):
return jsonify(error="URL must include http:// or https://"), 400
with sqlite3.connect("urls.db") as db:
# generate codes until one is unique
while True:
code = new_code()
try:
db.execute("INSERT INTO urls(code, url) VALUES(?, ?)",
(code, long_url))
break
except sqlite3.IntegrityError:
continue
return jsonify(short=f"{request.host_url}{code}")
@app.get("/<code>")
def follow(code):
with sqlite3.connect("urls.db") as db:
row = db.execute("SELECT url FROM urls WHERE code=?", (code,)).fetchone()
if not row:
return "Not found", 404
db.execute("UPDATE urls SET clicks = clicks + 1 WHERE code=?", (code,))
return redirect(row[0], code=302)
if __name__ == "__main__":
init_db()
app.run(debug=True)Test it:
curl -X POST http://localhost:5000/shorten -H "Content-Type: application/json" \
-d '{"url":"https://python.org"}'
# {"short":"http://localhost:5000/aB3xY9"}
curl -I http://localhost:5000/aB3xY9
# HTTP/1.1 302 FOUND
# Location: https://python.orgcurl -X POST http://localhost:5000/shorten -H "Content-Type: application/json" \
-d '{"url":"https://python.org"}'
# {"short":"http://localhost:5000/aB3xY9"}
curl -I http://localhost:5000/aB3xY9
# HTTP/1.1 302 FOUND
# Location: https://python.orgWhy base62
- 62 chars (
0–9 a–z A–Z0–9 a–z A–Z) is URL-safe and case-sensitive. - 6 characters =
62 ** 6 ≈ 57 billion62 ** 6 ≈ 57 billionpossible short URLs. - 8 characters =
218 trillion218 trillion. Plenty.
Some shorteners use a counter (auto-increment ID) encoded as base62, which guarantees uniqueness without retries.
Validating URLs Properly
User input is a frequent vector for open-redirect attacks. Validate:
from urllib.parse import urlparse
def is_valid_url(u: str) -> bool:
try:
p = urlparse(u)
return p.scheme in {"http", "https"} and bool(p.netloc) and "." in p.netloc
except Exception:
return Falsefrom urllib.parse import urlparse
def is_valid_url(u: str) -> bool:
try:
p = urlparse(u)
return p.scheme in {"http", "https"} and bool(p.netloc) and "." in p.netloc
except Exception:
return FalseReject javascript:javascript:, data:data:, file:file: schemes — they would let an attacker craft phishing-style short links.
Common Mistakes
| Problem | Cause | Fix |
|---|---|---|
pyshorteners.exceptions.ShorteningErrorExceptionpyshorteners.exceptions.ShorteningErrorException | Provider down, malformed URL, rate-limited | Catch and retry/back off |
| Same short code generated for two URLs | Did not check uniqueness | INSERTINSERT with PRIMARY KEYPRIMARY KEY on code → catch IntegrityErrorIntegrityError |
| Redirect goes nowhere | Returned a 200 with the URL in the body | Return redirect(url, code=302)redirect(url, code=302) |
Open redirect to javascript:javascript: | No URL validation | Whitelist http/httpshttp/https schemes only |
| Database lock errors | SQLite with concurrent writers | Use a real DB (Postgres) for multi-process deployments |
Variations to Try
1. Custom aliases
Let the user request /mylink/mylink instead of a random code:
alias = request.json.get("alias")
if alias and not db.execute("SELECT 1 FROM urls WHERE code=?", (alias,)).fetchone():
code = aliasalias = request.json.get("alias")
if alias and not db.execute("SELECT 1 FROM urls WHERE code=?", (alias,)).fetchone():
code = alias2. Expiration dates
Add expires_at TIMESTAMP NULLexpires_at TIMESTAMP NULL. On follow, return 410 Gone if expired.
3. Click analytics
You already store clicksclicks. Add a /stats/<code>/stats/<code> endpoint that returns total clicks plus, optionally, referrer breakdown.
4. QR code
import qrcode
img = qrcode.make(short_url)
img.save("link.png")import qrcode
img = qrcode.make(short_url)
img.save("link.png")5. Password-protected links
Add a password_hashpassword_hash column. Before redirecting, require the user to enter a password.
6. Bulk shortening
Accept a list of URLs and return short versions in one call.
7. Rate limiting
Use flask-limiterflask-limiter to cap to e.g. 10 requests/minute per IP.
8. Domain restrictions
Allow shortening only for specific domains — useful for company-internal use.
9. CLI
Wrap the API client with argparseargparse:
python shorten.py https://example.com --alias homepagepython shorten.py https://example.com --alias homepage10. Deployment
Run behind gunicorn + nginx, point a domain like short.example.comshort.example.com at it, and you have your own production shortener for ~$5/month.
Privacy and Security Notes
- Short URLs are public. Anyone who guesses the code can follow the link. Do not embed sensitive paths.
- Phishing risk. Short links hide their destination — bad actors abuse this. Consider a preview page (
/p/<code>/p/<code>) that shows the destination before redirecting. - Log retention. Server access logs reveal who clicked what. Decide your retention policy.
- Open-redirect is the classic shortener vulnerability — validate schemes carefully.
Real-World Applications
- Social-media sharing (Twitter character limits used to drive this).
- Email and SMS marketing — short, trackable URLs.
- QR codes that point to a short URL you can rebind later.
- Branded short links (e.g.,
nyti.msnyti.msfor the New York Times). - Internal tooling — share links to dashboards without copying 200-character URLs.
Educational Value
- Third-party APIs — calling, error handling, swap-ability.
- HTTP redirects — status codes and what they mean.
- Encoding schemes — base62 versus base64 versus hex.
- Database integrity — unique constraints and how to handle collisions.
- Web security basics — open redirect, input validation, abuse vectors.
Next Steps
- Build the Flask version above; deploy it somewhere free (Render, Fly.io).
- Add analytics with referrers, country (via MaxMind GeoIP), and time series.
- Add a web frontend with a form and a copy-to-clipboard button.
- Add authentication so each user only sees their own links.
- Layer on rate limiting and captcha before opening to the public.
Conclusion
You used a third-party shortener and learned how the actual underlying mechanism works — a database lookup and an HTTP 302. Building your own version is one of the most concise demonstrations of how the modern web composes simple parts into useful services. Full source on GitHub. Explore more web projects on Python Central Hub.
If this helped you, consider buying me a coffee ☕
Buy me a coffeeWas this page helpful?
Let us know how we did
