Skip to content

MarkFeder/url-shortener

Repository files navigation

🔗 URL Shortener

A fast, lightweight URL shortener built with Rust, Actix-web, and SQLite.

Features

  • Create short URLs from long URLs
  • 🎯 Custom short codes - use your own memorable codes
  • ✏️ Update destination - change a URL's target without losing the short code or click history
  • Expiring URLs - set expiration time in hours
  • 📊 Click tracking - track clicks with parsed browser, OS, device type, and referer domain
  • 📈 Analytics - view click logs, timeline charts, referrer/browser/device breakdowns
  • 🔧 Configurable click logging - enable/disable logging, set retention period
  • 🔐 Per-user authentication - email registration with API key management
  • 🔑 Multiple API keys - create, list, and revoke API keys per user
  • 👤 URL ownership - users can only access their own URLs
  • 📦 Bulk operations - create or delete up to 100 URLs in a single request
  • 🏷️ Tags/Categories - organize URLs with user-defined tags
  • 🔍 Search - find URLs by original URL or short code
  • 🛡️ Rate limiting - 60 requests/minute per IP to prevent abuse
  • In-memory caching - moka-based caching for URL redirects and API key validation
  • 📊 Prometheus metrics - monitor performance, cache efficiency, and business metrics
  • 📱 QR codes - generate QR codes for short URLs in PNG or SVG format
  • 🐳 Docker support - multi-stage Dockerfile and docker-compose for easy deployment
  • 🚀 Blazing fast - built with Rust and Actix-web
  • 💾 SQLite storage - no database server required
  • WAL mode - SQLite Write-Ahead Logging for better concurrency
  • 🔒 Atomic operations - transaction-based click recording for data consistency

Learning Concepts

This project demonstrates:

  1. Routing - RESTful API design with Actix-web
  2. IDs & Short Codes - Generating unique, URL-safe identifiers with nanoid
  3. Persistence - SQLite database with connection pooling (r2d2)
  4. Error Handling - Custom error types with proper HTTP responses
  5. Validation - Input validation with the validator crate
  6. Middleware - Logging and rate limiting middleware
  7. Rate Limiting - Request throttling with actix-governor
  8. Database Optimization - WAL mode for concurrent read/write performance
  9. Transactions - Atomic operations for data consistency
  10. Authentication - Per-user API key authentication with SHA-256 hashing
  11. Authorization - Resource ownership and access control
  12. Caching - In-memory caching with TTL and automatic invalidation
  13. Metrics - Prometheus metrics for observability and monitoring
  14. QR Code Generation - Creating QR codes with the qrcode crate
  15. Query Optimization - Avoiding N+1 queries with batch loading patterns
  16. Code Organization - Row mapping helpers and reusable ownership checks
  17. Containerization - Multi-stage Docker builds and docker-compose
  18. Testing - Unit and integration tests (192 tests) with shared test utilities
  19. Constants Management - Centralized magic numbers and configuration defaults
  20. Error Constructors - Domain-specific error factory methods for cleaner code
  21. User-Agent Parsing - Extracting browser, OS, and device type from UA strings with woothee
  22. Aggregated Analytics - SQL GROUP BY queries for timeline and breakdown reports

Project Structure

url-shortener/
├── Cargo.toml              # Dependencies and project metadata
├── Dockerfile              # Multi-stage Docker build
├── docker-compose.yml      # Docker Compose configuration
├── .dockerignore           # Docker build exclusions
├── .env                    # Environment configuration
├── .env.example            # Example configuration
├── .gitignore              # Git ignore rules
├── README.md               # This file
└── src/
    ├── main.rs             # Application entry point and server setup
    ├── auth.rs             # API key authentication extractor
    ├── test_utils.rs       # Shared test infrastructure (test-only)
    ├── infra/              # Cross-cutting infrastructure
    │   ├── cache.rs        # In-memory caching for URLs and API keys
    │   ├── config.rs       # Configuration management
    │   ├── constants.rs    # Centralized application constants
    │   ├── db.rs           # Database pool, WAL configuration, and migrations
    │   ├── errors.rs       # Custom error types and HTTP response mapping
    │   ├── metrics.rs      # Prometheus metrics for monitoring
    │   └── qr.rs           # QR code generation
    ├── models/             # DTOs and DB entities, split by domain
    │   ├── db.rs           # Database entity structs
    │   ├── url.rs          # URL request/response DTOs
    │   ├── auth.rs         # Auth request/response DTOs
    │   ├── tag.rs          # Tag request/response DTOs
    │   ├── analytics.rs    # Analytics query params and responses
    │   ├── bulk.rs         # Bulk operation DTOs
    │   ├── qr.rs           # QR code query params
    │   ├── common.rs       # Shared response wrappers
    │   └── validators.rs   # Custom validators
    ├── queries/            # SQL query constants, split by domain
    │   ├── schema.rs       # Schema DDL and migrations
    │   ├── users.rs        # User queries
    │   ├── api_keys.rs     # API key queries
    │   ├── urls.rs         # URL queries
    │   ├── click_logs.rs   # Click log queries
    │   └── tags.rs         # Tag and url_tags queries
    ├── services/           # Business logic, split by domain
    │   ├── helpers.rs      # Cross-cutting helpers (ownership, key gen)
    │   ├── auth.rs         # User registration and API key management
    │   ├── urls.rs         # URL CRUD, search, caching
    │   ├── tags.rs         # Tag CRUD and URL-tag associations
    │   ├── analytics.rs    # Click tracking and aggregation
    │   └── bulk.rs         # Bulk create/delete
    └── handlers/           # HTTP route handlers, split by domain
        ├── auth.rs         # Auth endpoints
        ├── urls.rs         # URL CRUD endpoints
        ├── tags.rs         # Tag endpoints
        ├── analytics.rs    # Analytics endpoints
        ├── bulk.rs         # Bulk operation endpoints
        ├── redirect.rs     # Public redirect handler
        └── health.rs       # Health check

Prerequisites

Getting Started

1. Clone or Open in RustRover

Open the project folder in JetBrains RustRover.

2. Build the Project

cargo build

3. Run the Server

cargo run

The server will start at http://localhost:8080

4. Run Tests

cargo test

API Reference

Authentication

All /api/* endpoints (except /api/auth/register) require authentication via API key.

Provide the API key using one of these headers:

  • X-API-Key: usk_your_key_here
  • Authorization: Bearer usk_your_key_here

Register User (Public)

POST /api/auth/register
Content-Type: application/json

{
    "email": "user@example.com"
}

Response (201 Created):

{
    "user_id": 1,
    "email": "user@example.com",
    "api_key": "usk_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6"
}

⚠️ Save your API key! It is only shown once at registration.


Create API Key (Authenticated)

POST /api/auth/keys
X-API-Key: usk_your_key_here
Content-Type: application/json

{
    "name": "CI/CD key"
}

Response (201 Created):

{
    "id": 2,
    "name": "CI/CD key",
    "api_key": "usk_x9y8z7w6v5u4t3s2r1q0p9o8n7m6l5k4",
    "created_at": "2024-01-01 12:00:00"
}

List API Keys (Authenticated)

GET /api/auth/keys
X-API-Key: usk_your_key_here

Response (200 OK):

{
    "keys": [
        {
            "id": 1,
            "name": "Default key",
            "created_at": "2024-01-01 12:00:00",
            "last_used_at": "2024-01-15 08:30:00",
            "is_active": true
        }
    ]
}

Revoke API Key (Authenticated)

DELETE /api/auth/keys/{id}
X-API-Key: usk_your_key_here

Response (200 OK):

{
    "message": "API key revoked successfully"
}

Create Short URL (Authenticated)

POST /api/shorten
X-API-Key: usk_your_key_here
Content-Type: application/json

{
    "url": "https://example.com/very/long/url/that/needs/shortening",
    "custom_code": "mylink",      # optional - custom short code
    "expires_in_hours": 24        # optional - URL expiration
}

Response (201 Created):

{
    "short_code": "mylink",
    "short_url": "http://localhost:8080/mylink",
    "original_url": "https://example.com/very/long/url/that/needs/shortening",
    "created_at": "2024-01-01 12:00:00",
    "expires_at": "2024-01-02 12:00:00"
}

Redirect to Original URL (Public)

GET /{short_code}

Response: 301 Permanent Redirect to the original URL


List Your URLs (Authenticated)

GET /api/urls?page=1&limit=20&sort=desc
X-API-Key: usk_your_key_here

Response (200 OK):

{
    "total": 42,
    "urls": [
        {
            "id": 1,
            "short_code": "abc123",
            "short_url": "http://localhost:8080/abc123",
            "original_url": "https://example.com",
            "clicks": 150,
            "created_at": "2024-01-01 12:00:00",
            "updated_at": "2024-01-15 08:30:00",
            "expires_at": null
        }
    ]
}

Note: Only returns URLs owned by the authenticated user.


Search URLs (Authenticated)

Search your URLs by original URL and/or short code (case-insensitive partial match).

# Search by original URL
GET /api/urls/search?q=github
X-API-Key: usk_your_key_here

# Search by short code
GET /api/urls/search?code=proj
X-API-Key: usk_your_key_here

# Search by both (must match both criteria)
GET /api/urls/search?q=github&code=proj
X-API-Key: usk_your_key_here

# With custom limit
GET /api/urls/search?q=example&limit=50
X-API-Key: usk_your_key_here

Query Parameters:

  • q: Search term for original URL (partial match, case-insensitive)
  • code: Search term for short code (partial match, case-insensitive)
  • limit: Maximum results (default: 20, max: 100)

At least one of q or code is required.

Response (200 OK):

{
    "total": 2,
    "urls": [
        {
            "id": 1,
            "short_code": "gh-proj",
            "short_url": "http://localhost:8080/gh-proj",
            "original_url": "https://github.com/myproject",
            "clicks": 42,
            "created_at": "2024-01-01 12:00:00",
            "updated_at": "2024-01-15 08:30:00",
            "expires_at": null
        }
    ]
}

Get URL Details (Authenticated)

GET /api/urls/{id}
X-API-Key: usk_your_key_here

Update URL Destination (Authenticated)

Change the destination URL while keeping the same short code, click history, and creation timestamp.

PUT /api/urls/{id}
X-API-Key: usk_your_key_here
Content-Type: application/json

{
    "url": "https://new-destination.com"
}

Response (200 OK):

{
    "id": 1,
    "short_code": "abc123",
    "short_url": "http://localhost:8080/abc123",
    "original_url": "https://new-destination.com",
    "clicks": 42,
    "created_at": "2024-01-01 12:00:00",
    "updated_at": "2024-01-15 09:00:00",
    "expires_at": null
}

Only the destination is mutable. The short code, click count, click history, and creation timestamp are preserved. The redirect cache is invalidated so the next hit serves the new destination immediately.


Get URL Statistics (Authenticated)

GET /api/urls/{id}/stats
X-API-Key: usk_your_key_here

Response (200 OK):

{
    "url": { ... },
    "recent_clicks": [
        {
            "id": 1,
            "url_id": 1,
            "clicked_at": "2024-01-15 08:30:00",
            "ip_address": "192.168.1.1",
            "user_agent": "Mozilla/5.0...",
            "referer": "https://google.com",
            "browser": "Chrome",
            "browser_version": "120.0.0.0",
            "os": "Windows 10",
            "device_type": "desktop",
            "referer_domain": "google.com"
        }
    ]
}

Click Timeline (Authenticated)

Get aggregated click counts grouped by time period.

GET /api/urls/{id}/analytics/timeline?period=daily&limit=7
X-API-Key: usk_your_key_here

Query Parameters:

  • period: hourly, daily (default), or weekly
  • limit: Maximum buckets to return (default: 30, max: 100)

Response (200 OK):

{
    "period": "daily",
    "data": [
        { "bucket": "2024-01-15", "count": 42 },
        { "bucket": "2024-01-14", "count": 38 },
        { "bucket": "2024-01-13", "count": 25 }
    ]
}

Referrer Breakdown (Authenticated)

Get top referring domains for a URL.

GET /api/urls/{id}/analytics/referrers?limit=10
X-API-Key: usk_your_key_here

Query Parameters:

  • limit: Maximum entries (default: 20, max: 100)

Response (200 OK):

{
    "data": [
        { "name": "google.com", "count": 85 },
        { "name": "twitter.com", "count": 32 },
        { "name": "direct", "count": 18 }
    ]
}

Browser Breakdown (Authenticated)

Get browser usage breakdown for a URL.

GET /api/urls/{id}/analytics/browsers?limit=10
X-API-Key: usk_your_key_here

Query Parameters:

  • limit: Maximum entries (default: 20, max: 100)

Response (200 OK):

{
    "data": [
        { "name": "Chrome", "count": 120 },
        { "name": "Firefox", "count": 45 },
        { "name": "Safari", "count": 30 }
    ]
}

Device Breakdown (Authenticated)

Get device type breakdown for a URL.

GET /api/urls/{id}/analytics/devices?limit=10
X-API-Key: usk_your_key_here

Query Parameters:

  • limit: Maximum entries (default: 20, max: 100)

Response (200 OK):

{
    "data": [
        { "name": "desktop", "count": 150 },
        { "name": "mobile", "count": 80 },
        { "name": "bot", "count": 12 },
        { "name": "other", "count": 3 }
    ]
}

Get QR Code for URL (Authenticated)

Generate a QR code image for a short URL.

GET /api/urls/{id}/qr
GET /api/urls/{id}/qr?format=svg
GET /api/urls/{id}/qr?format=png&size=512
X-API-Key: usk_your_key_here

Query Parameters:

  • format: Output format - png (default) or svg
  • size: Size in pixels (default: 256, min: 64, max: 1024)

Response:

  • image/png - PNG image of the QR code (default)
  • image/svg+xml - SVG image of the QR code

The QR code encodes the full short URL (e.g., http://localhost:8080/abc123).


Delete URL (Authenticated)

DELETE /api/urls/{id}
X-API-Key: usk_your_key_here

Bulk Create URLs (Authenticated)

Create multiple URLs in a single request (max 100).

POST /api/urls/bulk
X-API-Key: usk_your_key_here
Content-Type: application/json

{
    "urls": [
        { "url": "https://example1.com", "custom_code": "ex1" },
        { "url": "https://example2.com", "expires_in_hours": 24 },
        { "url": "https://example3.com" }
    ]
}

Response (201 Created - all succeeded):

{
    "status": "success",
    "total": 3,
    "succeeded": 3,
    "failed": 0,
    "results": [
        { "index": 0, "success": true, "data": { "short_code": "ex1", ... } },
        { "index": 1, "success": true, "data": { "short_code": "a1b2c3d", ... } },
        { "index": 2, "success": true, "data": { "short_code": "x9y8z7w", ... } }
    ]
}

Response (207 Multi-Status - partial success):

{
    "status": "partial_success",
    "total": 2,
    "succeeded": 1,
    "failed": 1,
    "results": [
        { "index": 0, "success": true, "data": { ... } },
        { "index": 1, "success": false, "error": { "code": "DUPLICATE_CODE", "message": "..." } }
    ]
}

Bulk Delete URLs (Authenticated)

Delete multiple URLs by ID in a single request (max 100).

DELETE /api/urls/bulk
X-API-Key: usk_your_key_here
Content-Type: application/json

{
    "ids": [1, 2, 3]
}

Response (200 OK - all succeeded):

{
    "status": "success",
    "total": 3,
    "succeeded": 3,
    "failed": 0,
    "results": [
        { "id": 1, "success": true },
        { "id": 2, "success": true },
        { "id": 3, "success": true }
    ]
}

Response (207 Multi-Status - partial success):

{
    "status": "partial_success",
    "total": 3,
    "succeeded": 2,
    "failed": 1,
    "results": [
        { "id": 1, "success": true },
        { "id": 2, "success": true },
        { "id": 999, "success": false, "error": { "code": "NOT_FOUND", "message": "..." } }
    ]
}

Create Tag (Authenticated)

POST /api/tags
X-API-Key: usk_your_key_here
Content-Type: application/json

{
    "name": "Important"
}

Response (201 Created):

{
    "id": 1,
    "name": "Important",
    "created_at": "2024-01-01 12:00:00"
}

List Tags (Authenticated)

GET /api/tags
X-API-Key: usk_your_key_here

Response (200 OK):

{
    "tags": [
        { "id": 1, "name": "Important", "created_at": "2024-01-01 12:00:00" },
        { "id": 2, "name": "Work", "created_at": "2024-01-01 12:00:00" }
    ]
}

Delete Tag (Authenticated)

DELETE /api/tags/{id}
X-API-Key: usk_your_key_here

Response (200 OK):

{
    "message": "Tag deleted successfully"
}

Note: Deleting a tag removes it from all associated URLs.


Add Tag to URL (Authenticated)

POST /api/urls/{id}/tags
X-API-Key: usk_your_key_here
Content-Type: application/json

{
    "tag_id": 1
}

Response (201 Created):

{
    "message": "Tag added to URL successfully"
}

Remove Tag from URL (Authenticated)

DELETE /api/urls/{id}/tags/{tag_id}
X-API-Key: usk_your_key_here

Response (200 OK):

{
    "message": "Tag removed from URL successfully"
}

Get URLs by Tag (Authenticated)

GET /api/tags/{id}/urls
X-API-Key: usk_your_key_here

Response (200 OK):

{
    "urls": [
        {
            "id": 1,
            "short_code": "abc123",
            "short_url": "http://localhost:8080/abc123",
            "original_url": "https://example.com",
            "clicks": 42,
            "created_at": "2024-01-01 12:00:00",
            "updated_at": "2024-01-15 08:30:00",
            "expires_at": null,
            "tags": [
                { "id": 1, "name": "Important", "created_at": "2024-01-01 12:00:00" }
            ]
        }
    ]
}

Health Check (Public)

GET /health

Response (200 OK):

{
    "status": "healthy",
    "version": "0.1.0"
}

Error Responses

The API returns consistent error responses with appropriate HTTP status codes:

Status Code Error Code Description
400 VALIDATION_ERROR Invalid input (bad URL format, invalid custom code)
401 UNAUTHORIZED Missing or invalid API key
403 FORBIDDEN Not allowed to access this resource
404 NOT_FOUND URL or resource not found
409 DUPLICATE_CODE Custom short code already exists
409 EMAIL_ALREADY_EXISTS Email is already registered
410 EXPIRED_URL URL has expired
429 RATE_LIMIT_EXCEEDED Too many requests
500 INTERNAL_ERROR Server error

Example error response:

{
    "error": "Missing API key. Provide via 'Authorization: Bearer <key>' or 'X-API-Key: <key>' header",
    "code": "UNAUTHORIZED"
}

Rate Limiting

The API is protected by rate limiting to prevent abuse:

  • Limit: 60 requests per minute per IP address
  • Burst: Up to 60 requests allowed in a burst
  • Response: Returns 429 Too Many Requests when limit is exceeded

Caching

The application uses in-memory caching (via moka) to optimize the two most frequently accessed operations:

Cached Operations

Operation Cache Key TTL Max Capacity
URL redirects (GET /{short_code}) short_code 5 min 10,000
API key validation key_hash 10 min 1,000

Cache Behavior

  • Cache Miss: On first access, data is fetched from the database and stored in cache
  • Cache Hit: Subsequent requests are served from cache without database queries
  • Automatic Expiration: Entries expire after their TTL (time-to-live)
  • Memory Bounded: Cache evicts oldest entries when max capacity is reached

Cache Invalidation

The cache is automatically invalidated when data changes:

Operation Invalidation
URL deleted Cache entry for that short_code removed
Bulk URL delete All affected short_code entries removed
API key revoked Cache entry for that key_hash removed
URL expired Detected on cache hit, entry removed

Performance Benefits

  • Reduced database load: Hot URLs and frequently-used API keys are served from memory
  • Lower latency: Cache hits avoid database round-trips
  • Lock-free concurrency: moka provides thread-safe access without locks

Prometheus Metrics

The application exposes Prometheus metrics at /metrics for monitoring and observability.

Accessing Metrics

# Get all metrics
curl http://localhost:8080/metrics

Available Metrics

HTTP Metrics (Automatic via actix-web-prom)

Metric Type Labels Description
url_shortener_http_requests_total Counter endpoint, method, status Total HTTP requests
url_shortener_http_request_duration_seconds Histogram endpoint, method Request latency distribution

Custom Business Metrics

Metric Type Labels Description
url_shortener_cache_hits_total Counter cache_type Cache hits (url, api_key)
url_shortener_cache_misses_total Counter cache_type Cache misses (url, api_key)
url_shortener_redirects_total Counter - Total URL redirects performed
url_shortener_urls_created_total Counter - Total URLs created
url_shortener_api_key_validations_total Counter result API key validations (success, invalid)

Example PromQL Queries

# Requests per second
rate(url_shortener_http_requests_total[5m])

# Redirects per second
rate(url_shortener_redirects_total[5m])

# Cache hit ratio for URL lookups
url_shortener_cache_hits_total{cache_type="url"} /
(url_shortener_cache_hits_total{cache_type="url"} + url_shortener_cache_misses_total{cache_type="url"})

# Error rate (5xx responses)
rate(url_shortener_http_requests_total{status=~"5.."}[5m])

# API key validation failure rate
rate(url_shortener_api_key_validations_total{result="invalid"}[5m])

Disabling Metrics

Set the environment variable to disable the metrics endpoint:

METRICS_ENABLED=false

Database Schema

The application uses SQLite with six tables:

users - Stores registered users

Column Type Description
id INTEGER Primary key
email TEXT Unique email address
created_at TEXT Registration timestamp

api_keys - Stores API keys for authentication

Column Type Description
id INTEGER Primary key
user_id INTEGER Foreign key to users
key_hash TEXT SHA-256 hash of API key
name TEXT Human-readable key name
created_at TEXT Creation timestamp
last_used_at TEXT Last usage timestamp
is_active INTEGER Whether key is active (1) or revoked (0)

urls - Stores shortened URLs

Column Type Description
id INTEGER Primary key
short_code TEXT Unique short code (indexed)
original_url TEXT Original URL
clicks INTEGER Click counter
created_at TEXT Creation timestamp
updated_at TEXT Last update timestamp
expires_at TEXT Optional expiration timestamp
user_id INTEGER Foreign key to users (owner)

click_logs - Stores click analytics (indexed on url_id and url_id, clicked_at)

Column Type Description
id INTEGER Primary key
url_id INTEGER Foreign key to urls
clicked_at TEXT Click timestamp
ip_address TEXT Visitor IP address
user_agent TEXT Raw browser user agent
referer TEXT Raw referring URL
browser TEXT Parsed browser name (e.g., Chrome, Firefox)
browser_version TEXT Parsed browser version
os TEXT Parsed operating system (e.g., Windows 10)
device_type TEXT Device category: desktop, mobile, bot, other
referer_domain TEXT Extracted domain from referer URL

tags - Stores user-defined tags

Column Type Description
id INTEGER Primary key
name TEXT Tag name (unique per user)
user_id INTEGER Foreign key to users
created_at TEXT Creation timestamp

url_tags - Junction table for URL-tag associations

Column Type Description
url_id INTEGER Foreign key to urls
tag_id INTEGER Foreign key to tags
PRIMARY KEY (url_id, tag_id) Composite key

Configuration

Environment variables (set in .env file):

Variable Default Description
DATABASE_URL urls.db SQLite database file path
HOST 127.0.0.1 Server host address
PORT 8080 Server port
BASE_URL http://localhost:8080 Base URL for generated short links
SHORT_CODE_LENGTH 7 Length of auto-generated codes
RUST_LOG info Logging level (debug, info, warn, error)
URL_CACHE_TTL_SECS 300 URL cache time-to-live in seconds (5 min)
URL_CACHE_MAX_CAPACITY 10000 Maximum number of URLs to cache
API_KEY_CACHE_TTL_SECS 600 API key cache time-to-live in seconds (10 min)
API_KEY_CACHE_MAX_CAPACITY 1000 Maximum number of API keys to cache
METRICS_ENABLED true Enable Prometheus metrics endpoint at /metrics
CLICK_LOGGING_ENABLED true Enable/disable click logging on redirects
CLICK_RETENTION_DAYS (none) Auto-delete click logs older than N days at startup
SHUTDOWN_TIMEOUT_SECS 30 Drain timeout for in-flight requests on SIGTERM/SIGINT

Docker

Quick Start with Docker Compose

The easiest way to run the application:

# Build and start the container
docker-compose up -d

# View logs
docker-compose logs -f

# Stop the container
docker-compose down

# Stop and remove data volume
docker-compose down -v

The application will be available at http://localhost:8080.

Building the Docker Image

# Build the image
docker build -t url-shortener .

# Run the container
docker run -d \
  --name url-shortener \
  -p 8080:8080 \
  -v url-shortener-data:/app/data \
  -e BASE_URL=http://localhost:8080 \
  url-shortener

Docker Configuration

The Docker image uses these default environment variables:

Variable Default Description
DATABASE_URL /app/data/urls.db SQLite database path (inside container)
HOST 0.0.0.0 Listen on all interfaces
PORT 8080 Server port
BASE_URL http://localhost:8080 Base URL for short links
RUST_LOG info Logging level
METRICS_ENABLED true Enable Prometheus metrics

Override any variable using -e flag or in docker-compose.yml.

Graceful Shutdown

The server listens for SIGTERM and SIGINT (Ctrl+C). On signal it stops accepting new connections and waits up to SHUTDOWN_TIMEOUT_SECS (default 30 s) for in-flight requests to finish before exiting.

When running under Docker, set the orchestrator's grace period to match. Docker's default stop_grace_period is 10 s, which would cut requests short — bump it in docker-compose.yml:

services:
  url-shortener:
    stop_grace_period: 35s   # SHUTDOWN_TIMEOUT_SECS + a small buffer

Production Deployment

For production, update BASE_URL to your domain:

docker run -d \
  --name url-shortener \
  -p 8080:8080 \
  -v url-shortener-data:/app/data \
  -e BASE_URL=https://your-domain.com \
  -e RUST_LOG=warn \
  url-shortener

Or modify docker-compose.yml:

environment:
  - BASE_URL=https://your-domain.com
  - RUST_LOG=warn

Health Check

The container includes a health check that polls /health every 30 seconds:

# Check container health status
docker inspect --format='{{.State.Health.Status}}' url-shortener

Data Persistence

SQLite data is stored in a Docker volume (url-shortener-data) mounted at /app/data. To backup:

# Backup database
docker cp url-shortener:/app/data/urls.db ./backup-urls.db

# Restore database
docker cp ./backup-urls.db url-shortener:/app/data/urls.db

Testing with cURL

# Register a new user (save the api_key from response!)
curl -X POST http://localhost:8080/api/auth/register \
  -H "Content-Type: application/json" \
  -d '{"email": "user@example.com"}'

# Create a short URL (replace YOUR_API_KEY)
curl -X POST http://localhost:8080/api/shorten \
  -H "Content-Type: application/json" \
  -H "X-API-Key: YOUR_API_KEY" \
  -d '{"url": "https://www.rust-lang.org/learn"}'

# Create with custom code
curl -X POST http://localhost:8080/api/shorten \
  -H "Content-Type: application/json" \
  -H "X-API-Key: YOUR_API_KEY" \
  -d '{"url": "https://docs.rs", "custom_code": "docs"}'

# Create with expiration
curl -X POST http://localhost:8080/api/shorten \
  -H "Content-Type: application/json" \
  -H "X-API-Key: YOUR_API_KEY" \
  -d '{"url": "https://temp.example.com", "expires_in_hours": 1}'

# List your URLs
curl -H "X-API-Key: YOUR_API_KEY" \
  http://localhost:8080/api/urls

# Search URLs by original URL
curl -H "X-API-Key: YOUR_API_KEY" \
  "http://localhost:8080/api/urls/search?q=github"

# Search URLs by short code
curl -H "X-API-Key: YOUR_API_KEY" \
  "http://localhost:8080/api/urls/search?code=docs"

# Search with both filters
curl -H "X-API-Key: YOUR_API_KEY" \
  "http://localhost:8080/api/urls/search?q=example&code=proj"

# Get URL details
curl -H "X-API-Key: YOUR_API_KEY" \
  http://localhost:8080/api/urls/1

# Update a URL's destination (preserves short_code and click history)
curl -X PUT http://localhost:8080/api/urls/1 \
  -H "Content-Type: application/json" \
  -H "X-API-Key: YOUR_API_KEY" \
  -d '{"url": "https://new-destination.com"}'

# Get URL statistics
curl -H "X-API-Key: YOUR_API_KEY" \
  http://localhost:8080/api/urls/1/stats

# Get QR code as PNG (default)
curl -H "X-API-Key: YOUR_API_KEY" \
  http://localhost:8080/api/urls/1/qr -o qrcode.png

# Get QR code as SVG
curl -H "X-API-Key: YOUR_API_KEY" \
  "http://localhost:8080/api/urls/1/qr?format=svg" -o qrcode.svg

# Get QR code with custom size
curl -H "X-API-Key: YOUR_API_KEY" \
  "http://localhost:8080/api/urls/1/qr?size=512" -o qrcode_large.png

# Delete a URL
curl -X DELETE -H "X-API-Key: YOUR_API_KEY" \
  http://localhost:8080/api/urls/1

# Bulk create URLs
curl -X POST http://localhost:8080/api/urls/bulk \
  -H "Content-Type: application/json" \
  -H "X-API-Key: YOUR_API_KEY" \
  -d '{"urls": [{"url": "https://example1.com"}, {"url": "https://example2.com", "custom_code": "ex2"}]}'

# Bulk delete URLs
curl -X DELETE http://localhost:8080/api/urls/bulk \
  -H "Content-Type: application/json" \
  -H "X-API-Key: YOUR_API_KEY" \
  -d '{"ids": [1, 2, 3]}'

# Test redirect (follow redirects) - no auth needed
curl -L http://localhost:8080/docs

# Create a tag
curl -X POST http://localhost:8080/api/tags \
  -H "Content-Type: application/json" \
  -H "X-API-Key: YOUR_API_KEY" \
  -d '{"name": "Important"}'

# List tags
curl -H "X-API-Key: YOUR_API_KEY" \
  http://localhost:8080/api/tags

# Add tag to URL
curl -X POST http://localhost:8080/api/urls/1/tags \
  -H "Content-Type: application/json" \
  -H "X-API-Key: YOUR_API_KEY" \
  -d '{"tag_id": 1}'

# Get URLs by tag
curl -H "X-API-Key: YOUR_API_KEY" \
  http://localhost:8080/api/tags/1/urls

# Remove tag from URL
curl -X DELETE -H "X-API-Key: YOUR_API_KEY" \
  http://localhost:8080/api/urls/1/tags/1

# Delete tag
curl -X DELETE -H "X-API-Key: YOUR_API_KEY" \
  http://localhost:8080/api/tags/1

# Create another API key
curl -X POST http://localhost:8080/api/auth/keys \
  -H "Content-Type: application/json" \
  -H "X-API-Key: YOUR_API_KEY" \
  -d '{"name": "Backup key"}'

# List your API keys
curl -H "X-API-Key: YOUR_API_KEY" \
  http://localhost:8080/api/auth/keys

# Revoke an API key
curl -X DELETE -H "X-API-Key: YOUR_API_KEY" \
  http://localhost:8080/api/auth/keys/2

Adding New Features

Here are some ideas for extending this project:

  1. Authentication - Add API keys or JWT authentication ✅ Done!
  2. QR Codes - Generate QR codes for short URLs ✅ Done!
  3. Custom Domains - Support multiple base URLs
  4. Bulk Operations - Create/delete multiple URLs at once ✅ Done!
  5. Search - Search URLs by original URL or code ✅ Done!
  6. Tags/Categories - Organize URLs with tags ✅ Done!
  7. Web UI - Add a frontend with HTML templates or SPA
  8. Caching - Add Redis or in-memory caching for hot URLs ✅ Done!
  9. Metrics - Add Prometheus metrics for monitoring ✅ Done!
  10. Docker Support - Add Dockerfile and docker-compose for deployment ✅ Done!

Dependencies

Crate Purpose
actix-web Web framework
actix-governor Rate limiting middleware
rusqlite SQLite database
r2d2 Connection pooling
serde Serialization
serde_json JSON support
nanoid Short code generation
chrono Date/time handling
validator Input validation
thiserror Error handling
env_logger Logging
url URL parsing and validation
regex Regular expressions
lazy_static Lazy static initialization
sha2 SHA-256 hashing for API keys
rand Random generation for API keys
moka In-memory caching with TTL support
actix-web-prom Prometheus metrics middleware
prometheus Prometheus metrics library
qrcode QR code generation
image Image encoding (PNG) for QR codes
woothee User-agent parsing (browser, OS, device type)

License

MIT License - feel free to use this project for learning and building!

About

A fast, lightweight URL shortener built with Rust, Actix-web, and SQLite.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors