A fast and scalable URL shortener built with FastAPI and PostgreSQL. This application provides a simple API to shorten long URLs and track visit statistics.
- URL Shortening: Convert long URLs into short, memorable codes
- Visit Tracking: Track the number of visits for each shortened URL
- Statistics API: Get detailed statistics about shortened URLs
- Request Logging: Comprehensive logging with IP tracking and timestamps
- Database Migrations: Alembic-based database schema management
- Load Testing: Built-in Locust configuration for performance testing
Create a .env
file in the project root:
# Database Configuration
DATABASE_URI=postgresql+asyncpg://postgres:postgres@localhost:5432/shortener
DATABASE_ENGINE_POOL_TIMEOUT=30
DATABASE_ENGINE_POOL_RECYCLE=3600
DATABASE_ENGINE_POOL_SIZE=10
DATABASE_ENGINE_MAX_OVERFLOW=20
DATABASE_ENGINE_POOL_PING=true
# Application Configuration
DEFAULT_DOMAIN=http://localhost:8000
SHORT_URL_LENGTH=6
- DATABASE_URI: PostgreSQL connection string
- DEFAULT_DOMAIN: Base domain for shortened URLs
- SHORT_URL_LENGTH: Length of generated short codes
- Pool settings: Database connection pool configuration
The project uses Ruff for linting and formatting:
# Install dev dependencies
uv sync --group dev
# Run linter
ruff check src/
# Auto-fix issues
ruff check --fix src/
# Format code
ruff format src/
# Install pre-commit
uv sync --group dev
# Set up hooks
pre-commit install
# Run hooks manually
pre-commit run --all-files
-
Clone the repository:
git clone https://github.com/erfan-rfmhr/url-shortener.git cd url-shortener
-
Start the services:
cd docker docker compose -f ./docker/docker-compose.yml up -d
This will start:
- PostgreSQL database on port 5432
- pgAdmin on port 5555 (admin@admin.com / admin)
- Main FastAPI application
The application will be available at
http://localhost:8000
.pgAdmin will be available at
http://localhost:5555
.
-
Install PostgreSQL and create a database named
shortener
-
Install dependencies using uv:
# Install uv pip install uv # Install project dependencies uv sync # Install development dependencies (optional) uv sync --group dev # Install test dependencies (optional) uv sync --group test
-
Run database migrations:
python src/migrate.py upgrade head
-
Start the application:
fastapi run src/main.py
curl -X POST "http://localhost:8000/api/v1/link/shorten" \
-H "Content-Type: application/json" \
-d '{"target_url": "https://www.example.com"}'
Response:
{
"shortened_url": "http://localhost:8000/abc123",
"target_url": "https://www.example.com"
}
curl "http://localhost:8000/api/v1/link/stats/abc123"
Response:
{
"short_url": "http://localhost:8000/abc123",
"target_url": "https://www.example.com",
"visits_count": 5,
"created_at": "2024-01-15T10:30:00"
}
Simply visit the shortened URL in your browser:
http://localhost:8000/abc123
This will redirect you to the original URL and increment the visit counter.
The application uses two main tables with a one-to-many relationship:
CREATE TABLE link (
id INTEGER PRIMARY KEY,
target TEXT NOT NULL, -- Original URL to redirect to
code TEXT NOT NULL UNIQUE, -- Short code (indexed)
created_at DATETIME NOT NULL, -- When the link was created
visits_count INTEGER DEFAULT 0 -- Cached visit count for performance
);
CREATE TABLE visit (
id INTEGER PRIMARY KEY,
link_id INTEGER NOT NULL, -- Foreign key to link.id
utm TEXT, -- UTM parameters (optional)
visited_at DATETIME NOT NULL -- When the visit occurred
);
- Link → Visit: One-to-Many relationship
- Each
Link
can have multipleVisit
records Visit.link_id
referencesLink.id
- The
visits_count
field inLink
is maintained for performance optimization
Database migrations are located in:
src/database/revisions/versions/
├── 1755184061_.py # Initial schema creation
├── 1755249632_add_visits_count_field.py # Added visits_count field
- Alembic config:
src/database/revisions/alembic.ini
- Environment setup:
src/database/revisions/env.py
Create a new migration:
python src/migrate.py revision --autogenerate -m "description of changes"
Apply migrations:
# Upgrade to latest
python src/migrate.py upgrade head
# Upgrade to specific revision
python src/migrate.py upgrade <revision_id>
# Downgrade one revision
python src/migrate.py downgrade -1
Check migration status:
python src/migrate.py current
python src/migrate.py history
url-shortener/
├── src/ # Main application source
│ ├── config/ # Configuration management
│ │ ├── __init__.py
│ │ └── conf.py # Settings and environment variables
│ ├── database/ # Database layer
│ │ ├── __init__.py
│ │ ├── core.py # Database engine and session management
│ │ └── revisions/ # Alembic migration files
│ │ ├── alembic.ini # Alembic configuration
│ │ ├── env.py # Migration environment setup
│ │ └── versions/ # Migration scripts
│ ├── link/ # Link domain module
│ │ ├── api/ # API layer
│ │ │ └── v1/ # API version 1
│ │ │ ├── routers.py # FastAPI route handlers
│ │ │ └── schemas.py # Pydantic models for API
│ │ ├── models.py # SQLModel database models
│ │ ├── repo.py # Database repository layer
│ │ └── service.py # Business logic layer
│ ├── logger.py # Logging configuration and decorators
│ ├── main.py # FastAPI application entry point
│ ├── migrate.py # Migration script wrapper
│ └── routers.py # Main router configuration
├── test/ # Test files
│ └── load_test.py # Locust load testing
├── docker/ # Docker configuration
│ └── docker-compose.yml # Docker services definition
│ └── Dockerfile # Dockerfile for building the application
├── .env.example # Environment variables example
├── logger.py # Logging configuration and decorators
├── migrate.py # Migration script wrapper
├── routers.py # Main router configuration
├── main.py # FastAPI application entry point
The application follows a layered architecture pattern:
- FastAPI routers handle HTTP requests and responses
- Pydantic schemas validate input/output data
- Route handlers delegate to service layer
- Business logic for URL shortening and visit tracking
- URL code generation using Base62 encoding with UUID
- Coordination between different repositories
- Data access abstraction over SQLModel/SQLAlchemy
- Database operations for Link and Visit entities
- Query optimization for statistics and aggregations
- SQLModel definitions for database tables
- Relationships between Link and Visit entities
- Field validation and constraints
- Environment-based settings using Pydantic Settings
- Database connection parameters
- Application configuration (domain, URL length, etc.)
/ # Root router (redirects)
├── /{short_code} # Redirect to original URL
└── /api/ # API routes
└── /v1/ # Version 1 API
└── /link/ # Link operations
├── routers.py # Route handlers
└── schemas.py # Pydantic models for API
- ShortenerService: Handles URL shortening logic
- VisitService: Manages visit tracking
- LinkRepo: Database operations for links
- VisitRepo: Database operations for visits
The application uses a decorator-based logging system implemented in src/logger.py
:
@log_request_info
async def redirect_to_url(request: Request, short_code: str, ...):
# Route handler logic
- Request logging: Captures IP address, timestamp, HTTP method, and path
- Proxy support: Handles
X-Forwarded-For
andX-Real-IP
headers - Error logging: Logs exceptions with stack traces
- Structured format: Consistent log format across the application
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
handlers=[logging.StreamHandler()],
)
2024-01-15 10:30:15,123 - src.routers - INFO - Request GET /abc123 from IP: 192.168.1.100 at 2024-01-15T10:30:15
2024-01-15 10:30:15,456 - src.routers - ERROR - Error updating visits: 123
- Default: Logs are output to console/stdout
- Production: Configure additional handlers for file logging or external services
- Customization: Modify
src/logger.py
to add file handlers, rotation, or external integrations
The project includes load testing capabilities using Locust.
-
Install test dependencies:
uv sync --group test
-
Start the application:
python src/main.py
-
Run Locust load tests:
# Basic load test locust -f test/load_test.py --host=http://localhost:8000 # Headless mode with specific parameters locust -f test/load_test.py --host=http://localhost:8000 \ --users 50 --spawn-rate 5 --run-time 60s --headless
-
Access Locust Web UI (if not using headless mode):
http://localhost:8089
Test documentation can be found in the tests folder.