Building a Custom URL Shortener with FastAPI and SQLite
Developing a custom URL shortener provides flexibility and control over links, particularly valuable for branding, internal systems, or specific tracking needs. A URL shortener service takes a long Uniform Resource Locator (URL) and converts it into a significantly shorter, unique identifier, often using a custom domain. When the short URL is accessed, the service redirects the user to the original long URL.
FastAPI, a modern, fast (high-performance) web framework for building APIs with Python 3.7+, and SQLite, a lightweight, serverless, self-contained SQL database engine, are well-suited for building such a service, especially for small to medium-scale applications or as a learning project. FastAPI’s speed is comparable to NodeJS and Go, while SQLite offers simplicity and ease of setup without requiring a separate database server.
Essential Concepts for a Custom URL Shortener
The core function of a URL shortener involves mapping a long URL to a short code and redirecting requests for the short code back to the original long URL.
- URL Mapping: The fundamental operation is creating a unique association between a lengthy URL and a brief, alphanumeric string (the “short code”). This mapping must be stored persistently.
- Short Code Generation: A method is needed to generate unique short codes. This can involve various strategies, such as incremental counters, random strings, or hashing techniques. The generated code must be unique for each long URL stored (or unique globally if allowing multiple short codes for the same long URL, though one-to-one is simpler initially).
- Database Storage: A database is required to store the mapping between the generated short code and the original long URL. SQLite is chosen for its simplicity and file-based nature, making setup straightforward.
- Web Framework: A web framework like FastAPI is used to handle incoming requests for both creating new short URLs and redirecting existing ones. FastAPI provides automatic documentation (Swagger UI/OpenAPI) and strong validation using Python type hints.
- Redirection: Upon receiving a request for a short URL, the service looks up the associated long URL in the database and issues an HTTP 301 (Permanent Redirect) or 307/308 (Temporary Redirect) response to the client’s browser, instructing it to navigate to the original URL.
Building the URL Shortener: A Step-by-Step Guide
Constructing the custom URL shortener involves setting up the project, defining the data model, creating the API endpoints, and handling database interactions.
1. Project Setup and Dependencies
First, set up a Python environment and install the necessary libraries.
# Create a project directorymkdir fastapi_url_shortenercd fastapi_url_shortener
# Set up a virtual environment (recommended)python -m venv venvsource venv/bin/activate # On Windows: `venv\Scripts\activate`
# Install dependenciespip install fastapi uvicorn sqlalchemy shortuuidfastapi: The web framework.uvicorn: An ASGI server to run the FastAPI application.sqlalchemy: An Object-Relational Mapper (ORM) to interact with the SQLite database.shortuuid: A library for generating unique, short, and unambiguous IDs.
2. Database Schema Definition with SQLAlchemy
Define the structure for storing the URL mappings. A simple table with two columns suffices: one for the short code and one for the original long URL.
Create a file database.py:
from sqlalchemy import create_engine, Column, Stringfrom sqlalchemy.ext.declarative import declarative_basefrom sqlalchemy.orm import sessionmaker
# Define the database URL. SQLite uses a file.# 'sqlite:///././shortener.db' refers to a file named shortener.db# in the current directory where the script is run.DATABASE_URL = "sqlite:///./shortener.db"
# create_engine creates an engine that connects to the database.# connect_args={"check_same_thread": False} is needed for SQLite# because SQLite is not designed for concurrent writes from multiple threads.# FastAPI runs using async which can involve multiple threads/processes,# so this flag prevents issues, though proper session handling is also key.engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False})
# declarative_base() returns a base class for declarative models.Base = declarative_base()
# Define the URL mapping model.class URLMapping(Base): __tablename__ = "url_mappings" # Table name in the database
# Short code column - acts as the primary key short_code = Column(String, primary_key=True, index=True) # Original long URL column long_url = Column(String, index=True)
# Create a SessionLocal class to create database sessions.# autoflush=False: Prevents SQLAlchemy from flushing changes to the DB# until session.commit() or session.flush() is called.# autocommit=False: Prevents SQLAlchemy from committing after every operation.# bind=engine: Binds the session to our database engine.SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
# Function to create all tables defined by Base (our URLMapping model)def create_db_tables(): Base.metadata.create_all(bind=engine)
# Dependency to get a database session (for use in FastAPI endpoints)def get_db(): db = SessionLocal() try: yield db # Provide the session to the calling function finally: db.close() # Ensure the session is closed after the request is finishedRun the create_db_tables() function once to initialize the database file and table. You can do this in your main FastAPI file before starting the server, or via a separate script.
3. FastAPI Application and Endpoints
Create the main FastAPI application file, e.g., main.py.
import shortuuidfrom fastapi import FastAPI, Depends, HTTPException, Request, statusfrom sqlalchemy.orm import Sessionfrom sqlalchemy.exc import IntegrityErrorfrom fastapi.responses import RedirectResponse
# Import database setup and modelfrom database import create_db_tables, get_db, URLMapping
# Initialize the database tables (run this once)create_db_tables()
app = FastAPI()
# --- Endpoint to create a short URL ---# Expects a POST request with the long URL in the request body.@app.post("/shorten")async def create_short_url(request: Request, db: Session = Depends(get_db)): try: # Read the request body directly as text long_url = await request.body() long_url = long_url.decode("utf-8").strip() # Decode bytes to string
# Basic validation: Ensure long_url is not empty and starts with http(s) if not long_url: raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="No URL provided") if not long_url.startswith(("http://", "https://")): # Simple correction for common cases, a more robust solution might validate further long_url = "http://" + long_url
# Generate a unique short code # Retry generating code if it already exists (highly unlikely with shortuuid but good practice) max_retries = 5 for _ in range(max_retries): # Generate a short, unique, URL-friendly ID short_code = shortuuid.uuid()[:8] # Using first 8 chars for brevity
# Check if this code already exists in the database existing_mapping = db.query(URLMapping).filter(URLMapping.short_code == short_code).first()
if not existing_mapping: # Code is unique, create the mapping new_mapping = URLMapping(short_code=short_code, long_url=long_url) db.add(new_mapping) try: db.commit() db.refresh(new_mapping) # Refresh to get any database-generated defaults/values if needed # Construct the full short URL (assuming running on localhost:8000 for example) # In a real deployment, this would use the deployed domain short_url = f"http://localhost:8000/{short_code}" return {"short_url": short_url, "long_url": long_url} except IntegrityError: # This catches the unlikely case where another process created the same short_code db.rollback() # Roll back the transaction continue # Try generating another code except Exception as e: db.rollback() raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Database error: {e}")
# If unable to generate a unique code after retries raise HTTPException(status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail="Could not generate a unique short code")
except HTTPException as http_exc: raise http_exc # Re-raise intended HTTP exceptions except Exception as e: # Catch any other unexpected errors raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"An unexpected error occurred: {e}")
# --- Endpoint to redirect based on short code ---# Uses a path parameter to capture the short code.@app.get("/{short_code}")def redirect_to_long_url(short_code: str, db: Session = Depends(get_db)): # Look up the short code in the database url_mapping = db.query(URLMapping).filter(URLMapping.short_code == short_code).first()
# If mapping exists, redirect if url_mapping: return RedirectResponse(url=url_mapping.long_url) else: # If mapping not found, return 404 Not Found raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Short URL not found")
# Optional: Root endpoint for basic info or documentation link@app.get("/")def read_root(): return {"message": "FastAPI URL Shortener. Visit /docs for API documentation."}4. Running the Application
With the files created, run the application using Uvicorn.
uvicorn main:app --reloadThis starts the server, typically at http://127.0.0.1:8000. The --reload flag is useful during development to automatically restart the server on code changes.
Access http://127.0.0.1:8000/docs in a web browser to see the automatically generated API documentation (Swagger UI), which can be used to test the /shorten endpoint.
Real-World Application Examples
A custom URL shortener built with FastAPI and SQLite, while lightweight, serves various practical purposes:
- Internal Link Management: Companies can use it to create memorable, short links for internal resources like project dashboards, documentation wikis, or shared drive locations, replacing long, unwieldy internal URLs.
- Branded Links: Organizations can use a custom domain (e.g.,
mybrand.link/xyz) instead of generic shortener domains. This enhances brand visibility and trust. While this requires domain configuration and deployment beyond the scope of this basic setup, the FastAPI application is the core service. - Simplified Sharing: For specific applications where users need to share links generated within the application, a custom shortener provides a seamless way to create and manage these links directly.
Example Usage:
- Shortening a URL:
- Make a POST request to
http://127.0.0.1:8000/shorten. - Include the long URL in the request body (as plain text).
- Example using
curl:Terminal window curl -X POST http://127.0.0.1:8000/shorten -d "https://www.example.com/very/long/page/path/for/article" - Expected response (example):
{"short_url": "http://localhost:8000/AbCdEfG1","long_url": "https://www.example.com/very/long/page/path/for/article"}
- Make a POST request to
- Accessing the Short URL:
- Open a web browser or use
curlto access the generated short URL: http://127.0.0.1:8000/AbCdEfG1- The server looks up
AbCdEfG1, finds the corresponding long URL, and issues a redirect, causing the browser to navigate tohttps://www.example.com/very/long/page/path/for/article.
- Open a web browser or use
This example demonstrates the core functionality. For production use, enhancements like input validation, error handling, rate limiting, and deployment to a server with a custom domain would be necessary. SQLite performs well for applications with moderate read traffic and low concurrent write traffic, suitable for many internal tools or small public services.
Key Takeaways
- Building a custom URL shortener provides control over link appearance and management.
- FastAPI offers a high-performance, developer-friendly framework for building the API.
- SQLite serves as a simple, file-based database solution ideal for smaller projects or rapid prototyping.
- The core mechanism involves generating a unique short code, storing the mapping between the short code and the long URL in a database (SQLite via SQLAlchemy), and redirecting requests for the short code.
- SQLAlchemy provides an Object-Relational Mapper (ORM) layer, simplifying database interactions in Python.
- The project structure includes defining database models, creating FastAPI endpoints for shortening and redirection, and setting up database sessions.
- Running the application with Uvicorn makes the API accessible for use and testing.
- Custom shorteners are useful for internal tools, branding, and controlled link distribution.