SnapIT is a highly scalable, full-stack URL shortener built with FastAPI, PostgreSQL, Redis, Next.js, and Tailwind CSS. It features Redis cache-aside low-latency redirects, race-safe custom aliases, per-IP rate limiting, non-blocking click analytics, and graceful HTTP 410 expiration handling.
┌─────────┐ Miss ┌────────────┐
┌────────┐ GET │ ├────────────────►│ │
│ Client ├──────►│ Redis │ │ PostgreSQL │
└────────┘ │ Cache │◄────────────────┤ DB │
└────┬────┘ Repopulate └──────┬─────┘
│ │
Hit │ (302 Redirect) │ Async Update
▼ ▼
┌───────────┐ ┌─────────────┐
│ Redirect │ │ Background │
│ completed │ │ Analytics │
└───────────┘ └─────────────┘
- Key Features
- API Reference
- Database Schema
- Quick Start (Local Development)
- Running Tests
- Project Structure
- Environment Configuration
- Free-Tier Deployment Guide
- Production Notes
- Low-Latency Redirects: Utilizes Redis cache-aside architecture. The database is exclusively touched on a cache miss.
- Race-Safe Aliases: Custom aliases are enforced for uniqueness at the database layer to prevent race conditions.
- Rate Limiting: Fixed-window per-IP rate limiting backed by Redis, returning
429 Too Many RequestswithRetry-Afterheaders. - Async Analytics: Click-counts and
last_accessedtimestamps are recorded via fire-and-forget background tasks so redirects never block. - Graceful Expiration: Supports TTL on URLs, returning an
HTTP 410 Gonestatus for expired links. - Containerized: Fully packaged with Docker Compose for a one-command local development environment.
| Method | Path | Description |
|---|---|---|
POST |
/api/shorten |
Create a short URL |
GET |
/api/analytics/{id} |
Retrieve click-count and timestamps |
GET |
/{short_id} |
302 Redirect (404 missing, 410 expired) |
GET |
/health/live |
Liveness probe |
GET |
/health/ready |
Readiness probe (DB + Redis check) |
GET |
/docs |
OpenAPI Swagger UI |
Click to view POST /api/shorten Payload & Response
Request Payload:
{
"original_url": "https://example.com/very/long/path",
"custom_alias": "my-link",
"expires_in_days": 30
}Response (201 Created):
{
"short_id": "my-link",
"short_url": "http://localhost:8000/my-link",
"original_url": "https://example.com/very/long/path",
"custom_alias": "my-link",
"created_at": "2026-04-23T10:15:00Z",
"expires_at": "2026-05-23T10:15:00Z"
}Note: Errors handle 400 invalid URL/alias, 409 alias taken, and 429 rate-limited.
Table: urls
| Column | Type | Notes |
|---|---|---|
id |
bigint |
PK, autoincrement |
original_url |
varchar(2048) |
Not null |
short_id |
varchar(64) |
Unique, indexed (hot-path lookup) |
custom_alias |
varchar(64) |
Unique, indexed, nullable |
created_at |
timestamptz |
Default now() |
expires_at |
timestamptz |
Indexed (for cleanup jobs), nullable |
click_count |
integer |
Default 0, incremented via atomic UPDATE |
last_accessed_at |
timestamptz |
Nullable |
The entire stack is containerized for zero-friction setup.
# Clone the repository
git clone https://github.com/YOUR_USERNAME/url-shortener.git
cd url-shortener
# Boot the stack
docker compose up --buildAccess Points:
- Frontend: http://localhost:3000
- Backend API: http://localhost:8000
- Swagger UI: http://localhost:8000/docs
- Postgres:
localhost:5432(user: postgres / pass: postgres) - Redis:
localhost:6379
(Note: The backend auto-creates DB tables on the first boot for local dev convenience).
The test suite intelligently swaps Postgres for SQLite (aiosqlite) and Redis for fakeredis, allowing tests to run entirely offline.
cd backend
# Create and activate virtual environment
python -m venv .venv
source .venv/bin/activate # On Windows use: .venv\Scripts\activate
# Install dependencies
pip install -r requirements.txt
pip install fakeredis pytest
# Run tests
pytest -qurl-shortener/
├── backend/
│ ├── app/
│ │ ├── main.py # FastAPI entrypoint + lifespan
│ │ ├── core/ # config, utils, exceptions
│ │ ├── db/ # async SQLAlchemy engine + session
│ │ ├── models/ # ORM models
│ │ ├── schemas/ # Pydantic request/response models
│ │ ├── services/ # cache, rate limiter, business logic
│ │ └── routes/ # shorten, redirect, health, deps
│ ├── tests/ # pytest suite (SQLite + fakeredis)
│ └── Dockerfile
├── frontend/
│ ├── pages/ # _app.js, index.js
│ ├── components/ # ShortenerForm, ThemeToggle
│ ├── styles/globals.css
│ └── Dockerfile
└── docker-compose.yml
Configuration is managed via environment variables. See backend/.env.example and frontend/.env.example.
DATABASE_URL: Async SQLAlchemy URL (postgresql+asyncpg://...)REDIS_URL: Redis connection string (rediss://...for TLS)RATE_LIMIT_MAX_REQUESTS/RATE_LIMIT_WINDOW_SECONDS: Per-IP limitsCACHE_DEFAULT_TTL: Redis TTL for short URLs in secondsSHORT_ID_LENGTH: Base62 length (Default 7 → 62⁷ ≈ 3.5T keyspace)CORS_ORIGINS: Comma-separated list of allowed origins
Deploying this stack for $0 is easy using modern cloud providers:
- PostgreSQL (Supabase): Create a project → Settings → Database → copy the "Connection pooling" URL. Change the prefix to
postgresql+asyncpg://. - Redis (Upstash): Create a free Redis DB. Copy the TLS URL (
rediss://default:<password>@<host>:6379). - Backend (Render): Create a Web Service → Connect repo → Root directory
backend/.- Env Vars:
DATABASE_URL,REDIS_URL,APP_ENV=production,BASE_URL,CORS_ORIGINS. - Health check path:
/health/ready
- Env Vars:
- Frontend (Netlify / Vercel): Import repo → Root directory
frontend/. Add Env Var:NEXT_PUBLIC_API_URL=https://<your-render-backend>.onrender.com.
Before scaling to heavy production traffic, consider the following:
- Migrations: The
init_db()call on startup is for local dev. In production, remove this call and manage schema changes strictly via Alembic. - Scaling: For High Availability (HA), run Uvicorn with multiple workers behind a reverse proxy. Because state lives in Postgres and Redis, the backend scales horizontally infinitely.
- Analytics Queuing: Consider moving analytics from in-process background tasks to a robust queue (RQ, Arq, Celery, or AWS SQS) to prevent dropping click records during abrupt pod crashes.
- Data Cleanup: Implement a nightly Cron job to delete rows where
expires_at < now() - interval '7 days'to prevent DB bloat.
Production traffic is instrumented via OpenTelemetry, with traces and metrics exported to Grafana Cloud over OTLP.
The service emits:
- Distributed traces — per-request flame graphs spanning FastAPI → SQLAlchemy → asyncpg → Redis
- Histogram metrics — request duration P50 / P95 / P99 by method and status code
- Counter metrics — request volume, error rate, cache operations
- MIT — do what you want.

