Skip to content

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 install and basic web concepts.

Install Dependencies

install
pip install pyshorteners
install
pip install pyshorteners

For the “build your own” section later:

install
pip install flask
install
pip install flask

Part 1 — Use a Third-Party Service

Getting started

  1. Create a folder named url-shortenerurl-shortener.
  2. Inside it, create urlshorter.pyurlshorter.py.

Write the code

URL ShortenerSource
URL Shortener
# 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 Shortener
# 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

command
C:\Users\Your Name\url-shortener> python urlshorter.py
Enter URL: https://www.example.com/very/long/url/with/many/parameters?param1=value1&param2=value2
Short URL: https://tinyurl.com/abc123def
command
C:\Users\Your Name\url-shortener> python urlshorter.py
Enter URL: https://www.example.com/very/long/url/with/many/parameters?param1=value1&param2=value2
Short URL: https://tinyurl.com/abc123def

Step-by-Step Explanation

1. Import the library

urlshorter.py
import pyshorteners
urlshorter.py
import pyshorteners

2. Validate and shorten

urlshorter.py
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)
urlshorter.py
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

urlshorter.py
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)
urlshorter.py
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

ServiceNeeds API key?
TinyURLNo
Is.gdNo
V.gdNo
Clck.ruNo
Chilp.itNo
BitlyYes
QR.netNo
OwlyYes
CuttlyYes

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

  1. User submits a long URL.
  2. Server stores { short_code → long_url }{ short_code → long_url } in a database.
  3. Server returns https://short.example.com/<short_code>https://short.example.com/<short_code>.
  4. 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

own_shortener.py
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)
own_shortener.py
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:

test
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.org
test
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.org

Why base62

  • 62 chars (09 a–z A–Z09 a–z A–Z) is URL-safe and case-sensitive.
  • 6 characters = 62 ** 657 billion62 ** 657 billion possible 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:

validate.py
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 False
validate.py
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 False

Reject javascript:javascript:, data:data:, file:file: schemes — they would let an attacker craft phishing-style short links.

Common Mistakes

ProblemCauseFix
pyshorteners.exceptions.ShorteningErrorExceptionpyshorteners.exceptions.ShorteningErrorExceptionProvider down, malformed URL, rate-limitedCatch and retry/back off
Same short code generated for two URLsDid not check uniquenessINSERTINSERT with PRIMARY KEYPRIMARY KEY on code → catch IntegrityErrorIntegrityError
Redirect goes nowhereReturned a 200 with the URL in the bodyReturn redirect(url, code=302)redirect(url, code=302)
Open redirect to javascript:javascript:No URL validationWhitelist http/httpshttp/https schemes only
Database lock errorsSQLite with concurrent writersUse 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.py
alias = request.json.get("alias")
if alias and not db.execute("SELECT 1 FROM urls WHERE code=?", (alias,)).fetchone():
    code = alias
alias.py
alias = request.json.get("alias")
if alias and not db.execute("SELECT 1 FROM urls WHERE code=?", (alias,)).fetchone():
    code = alias

2. 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

qr.py
import qrcode
img = qrcode.make(short_url)
img.save("link.png")
qr.py
import qrcode
img = qrcode.make(short_url)
img.save("link.png")

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:

cli
python shorten.py https://example.com --alias homepage
cli
python shorten.py https://example.com --alias homepage

10. 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.ms for 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 coffee

Was this page helpful?

Let us know how we did