Hello, tech enthusiasts! 👋
Your team sends long links all the time: in SMS messages, in printed flyers, in emails. A link like https://www.example.com/offers/summer/2026/product-page?ref=flyer is hard to type and looks messy. A short link like https://go.example.com/Xy7a9Q is much nicer.
An online service could do it, but then someone else holds your links and click numbers. Today we build our own in one Python file!
What you need
- Python 3.9 or newer (https://www.python.org)
- Flask, a small web framework:
pip install flask - SQLite, a database that lives in a single file (it is already part of Python)
A web framework is like a ready kitchen: the stove and the pots are there, you only cook the food.
Step 1: The plan
Our app does four things:
- Creates a short code for a long link.
- Sends visitors from the short link to the long link (a redirect). We use code 302, “temporary move”, so browsers do not remember it forever.
- Counts every click.
- Has a small admin page, protected by a password.
One safety rule: if any link can be shortened, spammers will use your domain to hide bad websites. So we only allow targets on our own domains.
Step 2: The code
Create a file called app.py:
import hmac
import os
import secrets
import sqlite3
from urllib.parse import urlparse
from flask import Flask, Response, abort, g, redirect, request
from markupsafe import escape
app = Flask(__name__)
DB_FILE = "links.db"
# Only links to these sites can be shortened
ALLOWED_HOSTS = {"example.com", "www.example.com", "shop.example.com"}
ADMIN_PASSWORD = os.environ.get("ADMIN_PASSWORD", "")
def init_db():
with sqlite3.connect(DB_FILE) as conn:
conn.execute("""CREATE TABLE IF NOT EXISTS links (
code TEXT PRIMARY KEY,
target TEXT NOT NULL,
clicks INTEGER NOT NULL DEFAULT 0)""")
def db():
# One database connection per request
if "db" not in g:
g.db = sqlite3.connect(DB_FILE)
g.db.row_factory = sqlite3.Row
return g.db
@app.teardown_appcontext
def close_db(error):
conn = g.pop("db", None)
if conn is not None:
conn.close()
def is_admin():
auth = request.authorization
if not ADMIN_PASSWORD or auth is None or auth.password is None:
return False
# compare_digest is safe against timing attacks
return hmac.compare_digest(auth.password.encode(), ADMIN_PASSWORD.encode())
def target_ok(url):
parts = urlparse(url)
return parts.scheme in ("http", "https") and parts.hostname in ALLOWED_HOSTS
@app.route("/admin", methods=["GET", "POST"])
def admin():
if not is_admin():
return Response("Login required", 401,
{"WWW-Authenticate": 'Basic realm="admin"'})
message = ""
if request.method == "POST":
target = request.form.get("target", "").strip()
if not target_ok(target):
message = "Only links to our own websites are allowed."
else:
for _ in range(5): # try again if the random code already exists
code = secrets.token_urlsafe(4)
try:
db().execute("INSERT INTO links (code, target) VALUES (?, ?)",
(code, target))
db().commit()
message = f"Short link: {request.host_url}{code}"
break
except sqlite3.IntegrityError:
continue
rows = db().execute(
"SELECT code, target, clicks FROM links ORDER BY clicks DESC").fetchall()
items = "".join(
f"<li>{escape(r['code'])} → {escape(r['target'])} ({r['clicks']} clicks)</li>"
for r in rows)
return f"""<h1>Link shortener</h1>
<p>{escape(message)}</p>
<form method="post">
<input name="target" size="60" placeholder="https://www.example.com/page">
<button>Shorten</button>
</form>
<ul>{items}</ul>"""
@app.route("/<code>")
def follow(code):
row = db().execute("SELECT target FROM links WHERE code = ?", (code,)).fetchone()
if row is None:
abort(404)
db().execute("UPDATE links SET clicks = clicks + 1 WHERE code = ?", (code,))
db().commit()
return redirect(row["target"], code=302)
init_db()
if __name__ == "__main__":
app.run()
A few notes:
secrets.token_urlsafe(4)makes a random code of about 6 letters and numbers, likeXy7a9Q.- The
?marks in the SQL keep the database safe from “SQL injection”, a trick where an attacker hides commands inside the text. escape()stops anyone from putting HTML or scripts into your admin page.urlparse(...).hostnamereads the real site name. So a tricky link likehttps://example.com@bad-site.testis blocked, because its real host isbad-site.test.
Step 3: Run it
Set a long password, then start the app:
export ADMIN_PASSWORD='choose-a-long-random-password'
python app.py
Open http://127.0.0.1:5000/admin in your browser. It asks for a username and password. Type any username and your password. Paste a long link, click Shorten, and try the short link. Then refresh the admin page and see the click count go up.
Step 4: Deploy it
The built-in Flask server is only for testing. For real use:
- Run the app with gunicorn:
pip install gunicorn, thengunicorn -w 2 -b 127.0.0.1:8000 app:app. - Put a web server like nginx in front, with HTTPS. This is a must. The admin password is sent with every request, and without HTTPS anyone on the network could read it.
- Use a short domain or subdomain, like
go.example.com. - Back up
links.dbregularly. It holds all your links.
Common problems
- The admin page keeps asking for the password. The
ADMIN_PASSWORDvariable is empty in the terminal where the app runs. Set it again in the same window. - “Only links to our own websites” for a correct link. Add the exact host name to
ALLOWED_HOSTS, for exampleshop.example.com. - “database is locked” errors. Many writers at once can cause this. Keep the number of workers small.
Wrap-up
In about 100 lines you have short links, click counts, a protected admin page and protection against abuse.
Next idea: add a column for the date of each click, and make a small weekly report of the top links for your marketing team. 🚀