On this page
article
Project: Flask URL Shortener
Build a URL shortener with Flask — SQLite database, short code generation, click analytics, and a REST API.
Build a complete web application that shortens URLs, tracks clicks, and exposes a JSON API — a classic Flask learning project.
What You’ll Build
GET / → Form to submit a URL
POST /shorten → Create short link, redirect to result page
GET /r/<code> → Redirect to original URL (tracks clicks)
GET /api/links → JSON list of all links
POST /api/shorten → JSON API to create short links
GET /stats/<code> → Click statistics for a link
Example: http://localhost:5000/r/aB3xK → https://example.com/very/long/url
Project Structure
url-shortener/
├── app/
│ ├── __init__.py
│ ├── models.py
│ ├── routes.py
│ └── utils.py
├── templates/
│ ├── base.html
│ ├── index.html
│ └── stats.html
├── config.py
├── requirements.txt
└── run.py
Setup
mkdir url-shortener && cd url-shortener
python -m venv .venv && source .venv/bin/activate
pip install flask flask-sqlalchemy
# requirements.txt
flask>=3.0
flask-sqlalchemy>=3.1
Database Model
# app/models.py
from datetime import datetime
from app import db
class Link(db.Model):
id = db.Column(db.Integer, primary_key=True)
original_url = db.Column(db.String(2048), nullable=False)
short_code = db.Column(db.String(10), unique=True, nullable=False, index=True)
clicks = db.Column(db.Integer, default=0)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
def to_dict(self):
return {
"id": self.id,
"original_url": self.original_url,
"short_code": self.short_code,
"short_url": f"/r/{self.short_code}",
"clicks": self.clicks,
"created_at": self.created_at.isoformat(),
}
App Factory
# app/__init__.py
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()
def create_app():
app = Flask(__name__)
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///links.db"
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
app.config["SECRET_KEY"] = "dev-secret-change-in-production"
db.init_app(app)
from app.routes import main
app.register_blueprint(main)
with app.app_context():
db.create_all()
return app
Short Code Generator
# app/utils.py
import secrets
import string
from app.models import Link
ALPHABET = string.ascii_letters + string.digits
def generate_short_code(length=6):
while True:
code = "".join(secrets.choice(ALPHABET) for _ in range(length))
if not Link.query.filter_by(short_code=code).first():
return code
def is_valid_url(url):
return url.startswith(("http://", "https://"))
Routes
# app/routes.py
from flask import Blueprint, render_template, request, redirect, jsonify, abort, url_for
from app import db
from app.models import Link
from app.utils import generate_short_code, is_valid_url
main = Blueprint("main", __name__)
@main.route("/")
def index():
links = Link.query.order_by(Link.created_at.desc()).limit(20).all()
return render_template("index.html", links=links)
@main.route("/shorten", methods=["POST"])
def shorten():
original_url = request.form.get("url", "").strip()
if not is_valid_url(original_url):
return render_template("index.html", error="Enter a valid http:// or https:// URL"), 400
link = Link(original_url=original_url, short_code=generate_short_code())
db.session.add(link)
db.session.commit()
return redirect(url_for("main.index"))
@main.route("/r/<code>")
def redirect_to(code):
link = Link.query.filter_by(short_code=code).first_or_404()
link.clicks += 1
db.session.commit()
return redirect(link.original_url)
@main.route("/stats/<code>")
def stats(code):
link = Link.query.filter_by(short_code=code).first_or_404()
return render_template("stats.html", link=link)
# REST API
@main.route("/api/links")
def api_list():
links = Link.query.order_by(Link.created_at.desc()).all()
return jsonify([link.to_dict() for link in links])
@main.route("/api/shorten", methods=["POST"])
def api_shorten():
data = request.get_json()
if not data or not is_valid_url(data.get("url", "")):
return jsonify({"error": "Valid url required"}), 400
link = Link(original_url=data["url"], short_code=generate_short_code())
db.session.add(link)
db.session.commit()
return jsonify(link.to_dict()), 201
Template
<!-- templates/index.html -->
{% extends "base.html" %}
{% block content %}
<h1>URL Shortener</h1>
{% if error %}<p class="error">{{ error }}</p>{% endif %}
<form method="POST" action="/shorten">
<input type="url" name="url" placeholder="https://example.com/long-url" required>
<button type="submit">Shorten</button>
</form>
<h2>Recent Links</h2>
<table>
<tr><th>Short</th><th>Original</th><th>Clicks</th></tr>
{% for link in links %}
<tr>
<td><a href="/r/{{ link.short_code }}">/r/{{ link.short_code }}</a></td>
<td>{{ link.original_url[:50] }}...</td>
<td><a href="/stats/{{ link.short_code }}">{{ link.clicks }}</a></td>
</tr>
{% endfor %}
</table>
{% endblock %}
Run
# run.py
from app import create_app
app = create_app()
if __name__ == "__main__":
app.run(debug=True)
python run.py
# Visit http://127.0.0.1:5000
Skills Applied
| Topic | Chapter |
|---|---|
| Flask basics | Getting Started |
| SQLAlchemy | Database |
| REST API | Flask REST API |
| Security | Security |
| Deployment | Flask Deployment |
Bonus Challenges
- Add user authentication — each user sees only their links
- Add link expiration (auto-delete after N days)
- Add custom short codes (user picks
/r/mycode) - Add QR code generation for short URLs (
pip install qrcode) - Write pytest tests with Flask test client
- Add rate limiting with
Flask-Limiter - Deploy with Docker
This project covers the core Flask workflow: models → routes → templates → API → deploy.