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/aB3xKhttps://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

  1. Add user authentication — each user sees only their links
  2. Add link expiration (auto-delete after N days)
  3. Add custom short codes (user picks /r/mycode)
  4. Add QR code generation for short URLs (pip install qrcode)
  5. Write pytest tests with Flask test client
  6. Add rate limiting with Flask-Limiter
  7. Deploy with Docker

This project covers the core Flask workflow: models → routes → templates → API → deploy.