Add URL shortening functionality with FastAPI

This commit is contained in:
2026-03-02 09:35:34 +02:00
parent 04a2f5a46b
commit d3778d1171
3 changed files with 57 additions and 26 deletions
+2 -1
View File
@@ -1,2 +1,3 @@
app/__pycache__ app/__pycache__
venv/ venv/
test.txt
+3 -3
View File
@@ -12,11 +12,11 @@ source venv/bin/activate
```bash ```bash
uvicorn app.main:app --reload uvicorn app.main:app --reload
``` ```
Then check: Then check:<br>
http://127.0.0.1:8000 http://127.0.0.1:8000<br>
http://127.0.0.1:8000/docs http://127.0.0.1:8000/docs
Test Short URL Endpoint Test Short URL Endpoint:<br>
http://127.0.0.1:8000/shorten http://127.0.0.1:8000/shorten
## Running a test ## Running a test
+52 -22
View File
@@ -1,13 +1,18 @@
from fastapi import FastAPI, HTTPException from fastapi import FastAPI, HTTPException
from fastapi.responses import RedirectResponse from fastapi.responses import RedirectResponse
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
from pydantic import BaseModel
from app.utils import generate_short_code from app.utils import generate_short_code
from dotenv import load_dotenv from pydantic import BaseModel, HttpUrl
import os from pydantic_settings import BaseSettings
import sqlite3 import sqlite3
import logging import logging
class AppSettings(BaseSettings):
base_url: str = "http://localhost:8000"
database_url: str = "urls.db"
settings = AppSettings() # Load environment variables
# Set up logging configuration # Set up logging configuration
logging.basicConfig(level=logging.INFO, logging.basicConfig(level=logging.INFO,
format="%(asctime)s - %(levelname)s - %(message)s") format="%(asctime)s - %(levelname)s - %(message)s")
@@ -17,8 +22,18 @@ logger = logging.getLogger(__name__)
@asynccontextmanager @asynccontextmanager
async def lifespan(app: FastAPI): async def lifespan(app: FastAPI):
conn = sqlite3.connect('urls.db') conn = sqlite3.connect('urls.db')
conn.execute('''CREATE TABLE IF NOT EXISTS urls conn = sqlite3.connect(settings.database_url)
(id INTEGER PRIMARY KEY, short_code TEXT, original_url TEXT, clicks INTEGER)''') conn.execute("""
CREATE TABLE IF NOT EXISTS urls (
id INTEGER PRIMARY KEY,
short_code TEXT UNIQUE,
original_url TEXT NOT NULL,
clicks INTEGER DEFAULT 0
);
""")
conn.execute("""
CREATE INDEX IF NOT EXISTS idx_short_code ON urls(short_code);
""")
conn.commit() conn.commit()
logger.info("Database initialized or already exists.") logger.info("Database initialized or already exists.")
yield yield
@@ -28,11 +43,10 @@ async def lifespan(app: FastAPI):
app = FastAPI(lifespan=lifespan) app = FastAPI(lifespan=lifespan)
def get_db_connection(): def get_db_connection():
conn = sqlite3.connect('urls.db') return sqlite3.connect(settings.database_url, check_same_thread=False)
return conn
class URLRequest(BaseModel): class URLRequest(BaseModel):
url: str url: HttpUrl
@app.get("/") @app.get("/")
def home(): def home():
@@ -41,36 +55,52 @@ def home():
@app.post("/shorten") @app.post("/shorten")
def shorten_url(request: URLRequest): def shorten_url(request: URLRequest):
logger.info(f"Request received to shorten URL: {request.url}")
short_code = generate_short_code()
conn = get_db_connection() conn = get_db_connection()
while conn.execute("SELECT 1 FROM urls WHERE short_code = ?", (short_code,)).fetchone(): existing = conn.execute(
"SELECT short_code FROM urls WHERE original_url = ?",
(str(request.url),)
).fetchone()
if existing:
conn.close()
return {"short_url": f"{settings.base_url}/{existing[0]}"}
short_code = generate_short_code()
while conn.execute(
"SELECT 1 FROM urls WHERE short_code = ?",
(short_code,)
).fetchone():
short_code = generate_short_code() short_code = generate_short_code()
conn.execute("INSERT INTO urls (short_code, original_url, clicks) VALUES (?, ?, ?)", conn.execute(
(short_code, request.url, 0)) "INSERT INTO urls (short_code, original_url, clicks) VALUES (?, ?, 0)",
(short_code, str(request.url))
)
conn.commit() conn.commit()
conn.close() conn.close()
short_url = f"{os.getenv('BASE_URL', 'http://localhost:8000')}/{short_code}" return {"short_url": f"{settings.base_url}/{short_code}"}
logger.info(f"Shortened URL created: {short_url}")
return {"short_url": short_url}
@app.get("/{short_code}") @app.get("/{short_code}")
def redirect_to_url(short_code: str): def redirect_to_url(short_code: str):
logger.info(f"Redirect request received for short_code: {short_code}")
conn = get_db_connection() conn = get_db_connection()
url_data = conn.execute("SELECT original_url, clicks FROM urls WHERE short_code = ?", (short_code,)).fetchone() url_data = conn.execute(
"SELECT original_url FROM urls WHERE short_code = ?",
(short_code,)
).fetchone()
if url_data is None: if url_data is None:
logger.warning(f"Shortened URL for {short_code} not found.") conn.close()
raise HTTPException(status_code=404, detail="Shortened URL not found") raise HTTPException(status_code=404, detail="Shortened URL not found")
original_url, clicks = url_data original_url = url_data[0]
conn.execute("UPDATE urls SET clicks = ? WHERE short_code = ?", (clicks + 1, short_code))
conn.execute(
"UPDATE urls SET clicks = clicks + 1 WHERE short_code = ?",
(short_code,)
)
conn.commit() conn.commit()
conn.close() conn.close()
logger.info(f"Redirecting to {original_url}. Total clicks: {clicks + 1}")
return RedirectResponse(url=original_url) return RedirectResponse(url=original_url)