URL shortener API (FastAPI) back-end by Postgres and Redis, with a batch worker that pre-generates short codes into a dedicated Redis instance.
This code is for study purposes ONLY !
| Component | Role |
|---|---|
| API | Create and resolve short links; cache reads/writes via Redis |
| Postgres | Durable storage (short_link table, partitioned by month) |
| Redis (cache) | Hot path for redirects |
| Redis (codes) | Pool of generated short codes consumed on create |
| Batch | Fills the codes Redis from a counter (runs as a Compose service locally) |
| Proxy | Nginx front door in local dev (:8080 → API :8000) |
- Python 3.14+
- Poetry
- Docker + Docker Compose (recommended for local dev)
Bring up the full local stack via the Makefile:
make startlocalThis will:
- Copy
docker/dev/env.example→.env(host tools useDATABASE_URLon127.0.0.1:5432) - Build and start Postgres, both Redis instances, API, proxy, and batch (
docker/dev/docker-compose.yml) - Wait for Postgres, then run
alembic upgrade headandscripts/create_short_link_partitions.py --ahead 2on the host
The API container entrypoint also runs migrations and partition creation before starting Uvicorn.
Stop and clean up:
make stoplocalWith the stack running:
poetry install| Service | Default URL / port |
|---|---|
| Proxy (recommended) | http://localhost:8080 (PROXY_PORT) |
| API (direct) | http://localhost:8000 (API_PORT) |
| Postgres | localhost:5432 |
| Redis cache | localhost:6379 |
| Redis codes | localhost:6380 |
Routes are under /v1. Use the proxy in local dev unless you need the API directly.
GET /v1/health
curl -sS "http://localhost:8080/v1/health"Returns {"status":"ok"} with HTTP 200.
POST /v1/shortlink
curl -sS -X POST "http://localhost:8080/v1/shortlink" \
-H "content-type: application/json" \
-d '{"url":"https://example.com","expires_at":"2026-12-31T00:00:00Z"}'Optional fields:
expires_at— ISO-8601 UTC timestamp; defaults to ~2 weeks from now when omitted.alias— custom short code (max 32 characters, base62 rules); when set, the code is taken from the request instead of the Redis codes pool.
Response envelope:
{"status":201,"data":{"code":"<code>"}}on success (default codes are 7 characters){"status":400,"data":{"error":"..."}}on validation failure
Example with a custom alias:
curl -sS -X POST "http://localhost:8080/v1/shortlink" \
-H "content-type: application/json" \
-d '{"url":"https://example.com","alias":"my-link"}'GET /v1/shortlink/{code}
On success the API returns HTTP 302 with a Location header (not a JSON body). Errors return JSON:
{"status":400,"data":{"error":"..."}}for invalid codes{"status":404,"data":{"error":"..."}}when not found
# Show status and Location without following the redirect
curl -sS -D - -o /dev/null "http://localhost:8080/v1/shortlink/ABC1234"# Follow redirect in the browser or with curl -L
curl -sS -L "http://localhost:8080/v1/shortlink/ABC1234"short_link is range-partitioned by created_at (monthly). Alembic defines create_short_link_partition(); creating partitions is a separate step:
make upgradelocal
poetry run python scripts/create_short_link_partitions.py --ahead 2make startlocal and the API container entrypoint run both automatically. For a specific month:
poetry run python scripts/create_short_link_partitions.py --month 2026-05Apply all migrations:
make upgradelocalCreate a new autogenerated revision:
make revisionlocal m="describe change"make unittest
make integrationtest
make e2etestIntegration and e2e tests use Testcontainers (TESTCONTAINERS_RYUK_DISABLED=true is set in the Makefile).
make lintformatmake loadtestcreate
make loadtestredirect
make stresstestcreate
make stresstestredirectTargets the proxy at http://localhost:8080 by default (see locust/).
Terraform runs in Docker via docker/terraform/docker-compose.yml (TF_WORKSPACE=staging):
| Target | Purpose |
|---|---|
make tfsetupinit / tfsetupvalidate / tfsetupapply |
Shared setup (infra/setup) |
make tfdeployinit / tfdeployvalidate / tfdeployapply |
App deploy (infra/deploy) |
See infra/setup and infra/deploy for AWS resources (ECS, RDS, load balancer, EventBridge batch schedule, etc.).