Introduction to URL Shorteners
A URL shortener is a web service that transforms long, unwieldy URLs into compact, easy-to-share links. Instead of sharing a 200-character URL filled with query parameters and tracking codes, users can share a short, memorable alias like https://short.link/abc123. When someone visits the shortened link, the service looks up the original URL in a database and redirects the visitor seamlessly.
URL shorteners matter for several practical reasons. They make links cleaner in printed materials and social media posts where character counts are limited. They improve aesthetics in email campaigns and SMS messages. They also enable click tracking and analytics, giving you insight into how many people engage with your links. Building your own URL shortener gives you full control over your links, eliminates reliance on third-party services that may change pricing or shut down, and lets you customize the experience to fit your brand or application.
Project Overview
🚀 Deploy your AI agent in 10 minutes
Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.
Try it free →In this tutorial, we'll build a fully functional URL shortener using Flask, a lightweight Python web framework, and SQLite, a serverless database that requires zero configuration. The application will allow users to submit a long URL and receive a shortened version. When anyone visits the short URL, they'll be instantly redirected to the original destination. We'll also track how many times each short link has been clicked.
Here's what the final project structure looks like:
url-shortener/
├── app.py # Main Flask application
├── templates/
│ ├── index.html # Home page with URL submission form
│ └── result.html # Page displaying the shortened URL
└── urls.db # SQLite database (created automatically)
Setting Up the Environment
Before writing any code, create a project directory and set up a Python virtual environment. This keeps dependencies isolated and prevents conflicts with other projects.
mkdir url-shortener
cd url-shortener
python -m venv venv
source venv/bin/activate # On Windows: venv\Scripts\activate
Install Flask. Flask includes everything we need — no additional packages are required since SQLite support is built into Python's standard library.
pip install flask
Create the project files. You can start with an empty app.py and a templates/ directory:
touch app.py
mkdir templates
touch templates/index.html
touch templates/result.html
Building the Flask Application: Complete Code
Let's build the entire application in one file. Below is the complete app.py. We'll walk through each section in detail afterward, but you can also copy this entire file and run it immediately.
import sqlite3
import string
import random
from flask import Flask, request, render_template, redirect, url_for, g
app = Flask(__name__)
# -------------------------------------------------------------------
# Database Helper Functions
# -------------------------------------------------------------------
DATABASE = 'urls.db'
def get_db():
"""Return a database connection for the current request context."""
db = getattr(g, '_database', None)
if db is None:
db = g._database = sqlite3.connect(DATABASE)
db.row_factory = sqlite3.Row
return db
@app.teardown_appcontext
def close_connection(exception):
"""Close the database connection at the end of each request."""
db = getattr(g, '_database', None)
if db is not None:
db.close()
def init_db():
"""Create the urls table if it doesn't already exist."""
with app.app_context():
db = get_db()
db.execute('''
CREATE TABLE IF NOT EXISTS urls (
id INTEGER PRIMARY KEY AUTOINCREMENT,
short_code TEXT UNIQUE NOT NULL,
original_url TEXT NOT NULL,
clicks INTEGER DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
''')
db.commit()
# -------------------------------------------------------------------
# Short Code Generator
# -------------------------------------------------------------------
def generate_short_code(length=7):
"""
Generate a random alphanumeric short code.
Uses uppercase letters, lowercase letters, and digits.
With length=7, this provides 62^7 ≈ 3.5 trillion unique combinations.
"""
characters = string.ascii_letters + string.digits # a-z, A-Z, 0-9 = 62 chars
code = ''.join(random.choice(characters) for _ in range(length))
return code
def get_unique_code(length=7):
"""
Generate a short code and verify it doesn't already exist in the database.
Keeps generating until a unique code is found.
"""
db = get_db()
while True:
code = generate_short_code(length)
cursor = db.execute('SELECT id FROM urls WHERE short_code = ?', (code,))
if cursor.fetchone() is None:
return code
# -------------------------------------------------------------------
# Flask Routes
# -------------------------------------------------------------------
@app.route('/', methods=['GET', 'POST'])
def index():
"""
Handle the home page.
GET: Display the URL submission form.
POST: Create a new shortened URL and redirect to the result page.
"""
if request.method == 'POST':
original_url = request.form.get('url', '').strip()
# Basic validation
if not original_url:
return render_template('index.html',
error='Please enter a URL.')
# Add http:// prefix if missing (for proper redirection)
if not original_url.startswith(('http://', 'https://')):
original_url = 'https://' + original_url
# Check if this URL already has a short code
db = get_db()
existing = db.execute(
'SELECT short_code FROM urls WHERE original_url = ?',
(original_url,)
).fetchone()
if existing:
short_code = existing['short_code']
else:
short_code = get_unique_code()
db.execute(
'INSERT INTO urls (short_code, original_url) VALUES (?, ?)',
(short_code, original_url)
)
db.commit()
return render_template('result.html',
short_code=short_code,
original_url=original_url,
short_url=request.host_url + short_code)
# GET request — show the form
return render_template('index.html')
@app.route('/')
def redirect_to_url(short_code):
"""
Handle incoming short URL visits.
Look up the short_code, increment the click counter, and redirect.
"""
db = get_db()
result = db.execute(
'SELECT original_url, clicks FROM urls WHERE short_code = ?',
(short_code,)
).fetchone()
if result is None:
return render_template('index.html',
error='Short URL not found.'), 404
# Increment click count
db.execute(
'UPDATE urls SET clicks = clicks + 1 WHERE short_code = ?',
(short_code,)
)
db.commit()
# Redirect to the original URL
return redirect(result['original_url'], code=302)
@app.route('/stats/')
def stats(short_code):
"""
Display click statistics for a given short code.
"""
db = get_db()
result = db.execute(
'SELECT original_url, short_code, clicks, created_at FROM urls WHERE short_code = ?',
(short_code,)
).fetchone()
if result is None:
return render_template('index.html',
error='Short URL not found.'), 404
return render_template('stats.html', stats=result)
# -------------------------------------------------------------------
# Application Entry Point
# -------------------------------------------------------------------
if __name__ == '__main__':
init_db()
app.run(debug=True, host='0.0.0.0', port=5000)
Understanding Each Component
Database Setup and Helper Functions
The database layer uses Flask's g object to store a per-request database connection. This pattern ensures each HTTP request gets its own connection, which is automatically closed when the request finishes via the teardown_appcontext decorator. Using sqlite3.Row as the row factory lets you access columns by name (like result['original_url']) instead of by index.
The urls table stores four key pieces of information:
- id — an auto-incrementing primary key for internal reference
- short_code — the unique alphanumeric string that appears in the shortened URL (e.g.,
abc1234) - original_url — the full destination URL the short link points to
- clicks — a counter tracking how many times the short link has been visited
- created_at — a timestamp recording when the link was created
The UNIQUE constraint on short_code prevents duplicate codes from ever being inserted, which is critical for correct routing.
Short Code Generation Logic
The short code generator creates random alphanumeric strings. With 62 possible characters (26 lowercase + 26 uppercase + 10 digits) and a length of 7 characters, the space contains 62⁷ — approximately 3.5 trillion — unique combinations. This makes collisions extremely rare, but the get_unique_code function still checks the database to guarantee uniqueness.
You can adjust the code length. Shorter codes (4–5 characters) are easier to type but run out of combinations faster. Longer codes (8–10 characters) provide near-infinite uniqueness at the cost of brevity. Seven characters strikes a practical balance for most applications.
The Main Routes Explained
The index route (/) handles both GET and POST requests. On GET, it renders the submission form. On POST, it validates the input, normalizes the URL by adding an https:// prefix if the user omitted it, checks for an existing short code to avoid duplicates, generates a new unique code if needed, and stores everything in the database. It then renders a result page showing the newly created short URL.
The redirect_to_url route (/<short_code>) captures the dynamic segment from the URL path. It queries the database for the matching short code, increments the click counter, and issues a 302 redirect to the original URL. The 302 status code indicates a temporary redirect, which is standard for URL shorteners and ensures browsers and search engines handle the redirect appropriately.
The stats route (/stats/<short_code>) provides a simple analytics view showing the original URL, the short code, the click count, and the creation timestamp.
Creating the HTML Templates
Flask uses Jinja2 templating by default. Templates live in the templates/ directory. Below are the three templates our application needs.
index.html — The URL Submission Form
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>URL Shortener</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #f4f6f9;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
}
.container {
background: white;
padding: 40px;
border-radius: 12px;
box-shadow: 0 4px 20px rgba(0,0,0,0.08);
width: 100%;
max-width: 520px;
}
h1 {
font-size: 28px;
margin-bottom: 8px;
color: #1a1a2e;
}
p.subtitle {
color: #666;
margin-bottom: 28px;
font-size: 15px;
}
label {
display: block;
font-weight: 600;
margin-bottom: 8px;
color: #333;
}
input[type="url"] {
width: 100%;
padding: 14px 16px;
font-size: 16px;
border: 2px solid #e0e0e0;
border-radius: 8px;
transition: border-color 0.2s;
}
input[type="url"]:focus {
outline: none;
border-color: #4a6cf7;
}
button {
margin-top: 20px;
width: 100%;
padding: 14px;
background: #4a6cf7;
color: white;
border: none;
border-radius: 8px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: background 0.2s;
}
button:hover { background: #3a56d4; }
.error {
background: #fee2e2;
color: #b91c1c;
padding: 12px 16px;
border-radius: 8px;
margin-bottom: 20px;
font-size: 14px;
}
</style>
</head>
<body>
<div class="container">
<h1>🔗 URL Shortener</h1>
<p class="subtitle">Paste a long URL and get a compact, shareable link in seconds.</p>
{% if error %}
<div class="error">{{ error }}</div>
{% endif %}
<form method="POST">
<label for="url">Your long URL</label>
<input
type="url"
id="url"
name="url"
placeholder="https://example.com/very-long-path?with=parameters"
required
>
<button type="submit">Shorten URL</button>
</form>
</div>
</body>
</html>
result.html — Displaying the Shortened URL
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Your Short URL</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #f4f6f9;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
}
.container {
background: white;
padding: 40px;
border-radius: 12px;
box-shadow: 0 4px 20px rgba(0,0,0,0.08);
width: 100%;
max-width: 520px;
text-align: center;
}
.success-icon {
font-size: 48px;
margin-bottom: 16px;
}
h1 {
font-size: 24px;
color: #1a1a2e;
margin-bottom: 20px;
}
.short-url-box {
background: #f0f4ff;
border: 2px dashed #4a6cf7;
border-radius: 8px;
padding: 20px;
margin: 20px 0;
word-break: break-all;
}
.short-url-box a {
color: #4a6cf7;
font-size: 18px;
font-weight: 600;
text-decoration: none;
}
.short-url-box a:hover {
text-decoration: underline;
}
.original-url {
color: #888;
font-size: 13px;
margin-top: 8px;
word-break: break-all;
}
.actions {
display: flex;
gap: 12px;
margin-top: 24px;
}
.btn {
flex: 1;
padding: 12px;
border-radius: 8px;
font-size: 14px;
font-weight: 600;
text-decoration: none;
cursor: pointer;
text-align: center;
}
.btn-primary {
background: #4a6cf7;
color: white;
border: none;
}
.btn-primary:hover { background: #3a56d4; }
.btn-secondary {
background: #f0f0f0;
color: #333;
border: none;
}
.btn-secondary:hover { background: #e0e0e0; }
.stats-link {
display: block;
margin-top: 16px;
color: #666;
font-size: 13px;
}
.stats-link a { color: #4a6cf7; }
</style>
</head>
<body>
<div class="container">
<div class="success-icon">✅</div>
<h1>Your Short URL is Ready!</h1>
<div class="short-url-box">
<a href="{{ short_url }}" target="_blank">{{ short_url }}</a>
<div class="original-url">Points to: {{ original_url }}</div>
</div>
<div class="actions">
<button class="btn btn-primary" onclick="copyToClipboard('{{ short_url }}')">
📋 Copy to Clipboard
</button>
<a href="/" class="btn btn-secondary">Shorten Another</a>
</div>
<div class="stats-link">
<a href="/stats/{{ short_code }}">View click statistics →</a>
</div>
</div>
<script>
function copyToClipboard(text) {
navigator.clipboard.writeText(text).then(() => {
const btn = document.querySelector('.btn-primary');
const originalText = btn.textContent;
btn.textContent = '✅ Copied!';
setTimeout(() => { btn.textContent = originalText; }, 2000);
});
}
</script>
</body>
</html>
stats.html — Click Analytics Page
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Link Statistics</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #f4f6f9;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
}
.container {
background: white;
padding: 40px;
border-radius: 12px;
box-shadow: 0 4px 20px rgba(0,0,0,0.08);
width: 100%;
max-width: 520px;
}
h1 {
font-size: 24px;
color: #1a1a2e;
margin-bottom: 24px;
}
.stat-card {
background: #f8fafc;
border: 1px solid #e2e8f0;
border-radius: 8px;
padding: 20px;
margin-bottom: 16px;
}
.stat-label {
font-size: 12px;
text-transform: uppercase;
letter-spacing: 0.5px;
color: #888;
margin-bottom: 4px;
}
.stat-value {
font-size: 20px;
font-weight: 700;
color: #1a1a2e;
}
.clicks-badge {
display: inline-block;
background: #4a6cf7;
color: white;
padding: 6px 16px;
border-radius: 20px;
font-size: 32px;
font-weight: 700;
}
.back-link {
display: block;
margin-top: 24px;
color: #4a6cf7;
text-decoration: none;
font-weight: 600;
}
.back-link:hover { text-decoration: underline; }
</style>
</head>
<body>
<div class="container">
<h1>📊 Link Statistics</h1>
<div class="stat-card">
<div class="stat-label">Short Code</div>
<div class="stat-value">{{ stats.short_code }}</div>
</div>
<div class="stat-card">
<div class="stat-label">Original URL</div>
<div class="stat-value" style="font-size:14px; word-break:break-all;">
{{ stats.original_url }}
</div>
</div>
<div class="stat-card">
<div class="stat-label">Total Clicks</div>
<div class="clicks-badge">{{ stats.clicks }}</div>
</div>
<div class="stat-card">
<div class="stat-label">Created</div>
<div class="stat-value" style="font-size:16px;">{{ stats.created_at }}</div>
</div>
<a href="/" class="back-link">← Back to Shortener</a>
</div>
</body>
</html>
Running the Application
With all files in place, start the Flask development server:
python app.py
You'll see output similar to:
* Running on http://0.0.0.0:5000
* Debug mode: on
Open your browser and navigate to http://localhost:5000. You'll see the URL submission form. Paste a long URL, click "Shorten URL," and you'll receive a short link like http://localhost:5000/xY7kLp2. Visiting that link redirects you to the original URL. The stats page at http://localhost:5000/stats/xY7kLp2 shows the click count.
How to Use the URL Shortener in Production
While the development server works for testing, a production deployment requires additional steps:
- Use a production WSGI server like Gunicorn (
gunicorn app:app) instead of Flask's built-in server - Set up a proper domain so your short URLs use a clean hostname like
https://short.example.com/abc123instead oflocalhost:5000 - Disable debug mode by removing
debug=Trueand settingapp.config['DEBUG'] = False - Add a reverse proxy like Nginx to handle SSL termination and static file serving
- Back up your SQLite database regularly — a single
urls.dbfile is easy to copy but easy to lose
Best Practices and Security Considerations
Validate and Sanitize URLs
Always validate URLs before storing them. The code above adds an https:// prefix if missing, but in production you should also validate that the URL has a valid structure. Consider using Python's urllib.parse module for more robust parsing:
from urllib.parse import urlparse
def is_valid_url(url):
try:
result = urlparse(url)
return all([result.scheme, result.netloc])
except Exception:
return False
Prevent Open Redirect Vulnerabilities
An open redirect occurs when an attacker crafts a URL that causes your shortener to redirect users to a malicious site. Since our shortener only redirects to URLs that were explicitly stored by users, the risk is lower, but you should still verify that stored URLs point to legitimate destinations. Consider maintaining a blocklist of suspicious domains.
Rate Limiting
Without rate limiting, a malicious actor could flood your service with URL creation requests, exhausting database storage or generating every possible short code. Implement rate limiting using Flask extensions like flask-limiter or by tracking request counts per IP address in a separate database table.
Short Code Predictability
The random generation approach using random.choice is suitable for most use cases. However, if you need cryptographically secure codes (for example, to prevent users from guessing valid short URLs), use secrets.choice from Python's secrets module instead of random.choice. The secrets module uses a cryptographically secure random number generator:
import secrets
def generate_secure_short_code(length=7):
characters = string.ascii_letters + string.digits
return ''.join(secrets.choice(characters) for _ in range(length))
Database Concurrency
SQLite handles concurrent reads well but serializes write operations. For high-traffic applications with many simultaneous URL creations, consider switching to PostgreSQL or MySQL. If you stick with SQLite, enable WAL (Write-Ahead Logging) mode for better concurrent performance:
db.execute('PRAGMA journal_mode=WAL')
Add Expiry for Short Links
Over time, your database may accumulate abandoned short links. Add an optional expiry feature by including an expires_at column and periodically cleaning up expired entries:
# Add to the schema
db.execute('ALTER TABLE urls ADD COLUMN expires_at TIMESTAMP')
# Cleanup function (run via a scheduled job)
def cleanup_expired():
db = get_db()
db.execute("DELETE FROM urls WHERE expires_at < datetime('now')")
db.commit()
Extending the Application
This foundation can be extended in many directions. You could add a REST API endpoint that accepts JSON and returns JSON, making the shortener usable programmatically. You could implement custom short codes so users can choose memorable aliases like /my-project. You could add user accounts with authentication so each user manages their own set of links. You could build a dashboard with charts showing click trends over time. The simplicity of Flask and SQLite makes these extensions straightforward to implement incrementally.
Conclusion
Building a URL shortener with Flask and SQLite is an excellent project that teaches core web development concepts: routing, database integration, templating, and redirect handling. The entire application fits in a single Python file and a few HTML templates, yet it's fully functional and ready for real-world use with the right production configuration. By following the patterns and best practices outlined in this tutorial, you now have a solid foundation that you can customize, extend, and deploy with confidence. Whether you need branded short links for marketing campaigns, compact URLs for social media, or simply want to understand how link shortening works under the hood, this implementation gives you a complete, working solution.