This document provides a comprehensive overview of the URL Shortener application architecture, designed to help contributors understand the system structure and design decisions.
┌─────────────────────────────────────────────────────────────────────────────┐
│ HTTP Layer │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌───────────┐ │
│ │ Static │ │ Admin UI │ │ API │ │ Health │ │
│ │ Files │ │ /admin │ │ Endpoints │ │ Check │ │
│ │ │ │ │ │ │ │ │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ └───────────┘ │
│ │ │
├─────────────────────────────────────────────────┼──────────────────────────┤
│ Middleware Layer │ │
│ │ │
│ ┌────────────────────┐ ┌────────────────────────▼─────────────────────────┐│
│ │ Rate Limiting │ │ API Key Authentication ││
│ │ (tower-governor) │ │ (Protected Routes Only) ││
│ └────────────────────┘ └────────────────────────┬─────────────────────────┘│
├──────────────────────────────────────────────────┼──────────────────────────┤
│ Application Layer │ │
│ │ │
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────▼─────────────┐ │
│ │ URL Redirect │ │ URL Shortening │ │ Template │ │
│ │ Handler │ │ Handler │ │ Rendering │ │
│ │ │ │ │ │ │ │
│ └─────────────────┘ └─────────────────┘ └───────────────────┘ │
│ │ │ │ │
├───────────┼─────────────────────┼─────────────────────┼─────────────────────┤
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌──────────────────────────────────────────────────────────────────────┐ │
│ │ Application State │ │
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────────────────┐│ │
│ │ │ Database │ │ API Key │ │ Template Directory ││ │
│ │ │ Handle │ │ │ │ ││ │
│ │ └─────────────┘ └─────────────┘ └─────────────────────────────────┘│ │
│ └──────────────────────────────────────────────────────────────────────┘ │
│ │ │
├───────────┼─────────────────────────────────────────────────────────────────┤
│ ▼ │
│ ┌──────────────────────────────────────────────────────────────────────┐ │
│ │ Database Layer │ │
│ │ │ │
│ │ ┌─────────────────┐ ┌─────────────────────────────────┐ │ │
│ │ │ UrlDatabase │◄─────────────┤ SQLite Implementation │ │ │
│ │ │ Trait │ │ │ │ │
│ │ │ │ │ ┌─────────────────────────────┐│ │ │
│ │ │ - insert_url() │ │ │ Connection Pool ││ │ │
│ │ │ - get_url() │ │ │ ││ │ │
│ │ │ │ │ └─────────────────────────────┘│ │ │
│ │ │ │ └─────────────────────────────────┘ │ │
│ │ │ │ │ │
│ │ │ │ ┌─────────────────────────────────┐ │ │
│ │ │ │◄─────────────┤ PostgreSQL Implementation │ │ │
│ │ │ │ │ │ │ │
│ │ │ │ │ ┌─────────────────────────────┐│ │ │
│ │ │ │ │ │ Connection Pool ││ │ │
│ │ │ │ │ │ ││ │ │
│ │ └─────────────────┘ │ └─────────────────────────────┘│ │ │
│ │ └─────────────────────────────────┘ │ │
│ └──────────────────────────────────────────────────────────────────────┘ │
│ │ │
└───────────────────────────────────────────┼─────────────────────────────────┘
▼
┌─────────────────┐
│ SQLite File │
│ database.db │
│ OR │
│ PostgreSQL DB │
└─────────────────┘
The main entry point orchestrates the application startup:
- Telemetry Initialization: Sets up structured logging with tracing
- Configuration Loading: Reads YAML configs and environment variables
- Application Bootstrap: Creates and starts the web server
- Graceful Shutdown: Handles SIGTERM/SIGINT signals
#[tokio::main]
async fn main() -> anyhow::Result<()> {
// Initialize logging → Load config → Build app → Run until stopped
}Multi-layered configuration management using Figment:
- Base Configuration:
configuration/base.yml - Environment Overrides:
configuration/local.ymlorconfiguration/production.yml - Environment Variables:
APP_*prefixed variables - Type Safety: Strongly typed configuration structs with validation
Configuration Hierarchy: Environment Variables > Environment File > Base File
Design Pattern: Repository Pattern with Trait Abstraction
#[async_trait]
pub trait UrlDatabase: Send + Sync {
async fn insert_url(&self, id: &str, url: &str) -> Result<(), DatabaseError>;
async fn get_url(&self, id: &str) -> Result<String, DatabaseError>;
}Benefits:
- Testability: Easy to mock for unit tests
- Flexibility: Can swap database implementations (PostgreSQL, MySQL, etc.)
- Type Safety: SQLx provides compile-time SQL validation
Current Implementations:
- SQLite: File-based database with connection pooling and automatic migrations
- PostgreSQL: Networked database with connection pooling and dedicated migrations
RESTful API design with clear separation of concerns:
POST /api/shorten(Protected) - Create shortened URLsGET /api/redirect/{id}- Redirect to original URLGET /api/health_check- Service health statusGET /admin- Admin web interface- Static Files - CSS/JS assets
Each handler follows a consistent pattern:
- Extract request data (path params, body, headers)
- Validate input (URL format, length limits, security checks)
- Interact with database layer
- Return structured response or error
Input Validation Features:
- URL Length Validation: Configurable maximum URL length (default: 2048 characters)
- URL Format Validation: RFC-compliant URL parsing using
urlcrate - Security Validation: Prevents malicious input and resource exhaustion
- Early Validation: Length checks before expensive parsing operations
Request Processing Pipeline:
Request → Request ID → Tracing → Rate Limiting → API Key Auth → Handler → Response
- Request ID Generation: UUID-based correlation IDs
- Distributed Tracing: Request lifecycle tracking
- Rate Limiting: Per-IP rate limiting using GCRA algorithm (tower-governor)
- API Key Authentication: Protects sensitive endpoints
- Error Handling: Converts errors to HTTP responses
Rate Limiting Features:
- Per-IP Enforcement: Individual rate limits for each client IP
- GCRA Algorithm: Generic Cell Rate Algorithm for smooth rate limiting
- Configurable: Requests per second and burst size configurable
- Selective: Only applies to URL shortening endpoints, not redirects/health checks
Shared State Pattern: Dependency injection container
pub struct AppState {
pub database: Arc<dyn UrlDatabase>,
pub api_key: Uuid,
pub template_dir: String,
}Benefits:
- Thread Safety: Arc allows sharing across async tasks
- Dependency Injection: Handlers receive dependencies through state
- Configuration: Runtime configuration accessible to all handlers
Centralized Error Management:
pub enum ApiError {
BadRequest(String),
NotFound(String),
Unauthorized(String),
// ... other variants
}Features:
- HTTP Status Mapping: Errors automatically convert to appropriate HTTP codes
- Error Chain Preservation: Full error context maintained for debugging
- Consistent API Responses: All errors return JSON envelope format
JSON Envelope Pattern:
{
"success": true|false,
"message": "descriptive message",
"status": 200,
"time": "2025-09-23T12:00:00Z",
"data": { ... } | null
}Benefits:
- Consistent Structure: All API responses follow same format
- Client-Friendly: Easy to parse and handle on frontend
- Debugging: Timestamps and messages aid troubleshooting
Tera Integration:
- Template Compilation: Templates compiled once at startup
- Inheritance: Base templates with block extensions
- Context Injection: Dynamic data binding
- Static Assets: CSS/JS served from
/static
1. POST /api/shorten with URL in body
2. Rate limiting middleware checks IP-based limits
3. API Key middleware validates x-api-key header
4. Handler validates URL length (max 2048 characters)
5. Handler parses and validates URL format
6. Generate unique 6-character ID (nanoid)
7. Store mapping in database
8. Return shortened URL: https://host/{id}
1. GET /api/redirect/{id}
2. Handler extracts ID from path
3. Database lookup for original URL
4. Return HTTP 308 Permanent Redirect
5. Browser follows redirect to original URL
1. GET /admin
2. Handler loads template context
3. Tera renders HTML with base template
4. Static CSS/JS loaded from /static
5. Return complete HTML page
Core URLs Table:
CREATE TABLE urls (
id TEXT PRIMARY KEY, -- 6-character nanoid
url TEXT NOT NULL, -- Original URL
owner_id TEXT NULL -- FK to users.id (optional)
);User Management Tables:
CREATE TABLE users (
id TEXT PRIMARY KEY, -- UUID for portability
email TEXT NOT NULL UNIQUE, -- User email address
password_hash TEXT NOT NULL, -- Secure password hash
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE sessions (
id TEXT PRIMARY KEY, -- Session UUID
user_id TEXT NOT NULL, -- FK to users.id
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
expires_at DATETIME NOT NULL, -- Session expiry
last_active_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
user_agent TEXT, -- Optional metadata
ip_address TEXT, -- Optional metadata
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);Design Decisions:
- Text IDs: Human-readable, URL-safe identifiers across all tables
- Optional User Association: URLs can be anonymous or user-owned
- Session Management: Foundation for user authentication system
- Backward Compatibility: Existing anonymous URLs remain functional
- Performance Indexes: Strategic indexes for common lookup patterns
- Presentation Layer: HTTP handlers and templates
- Business Logic Layer: URL shortening logic
- Data Access Layer: Database abstraction
- Infrastructure Layer: Configuration, logging, startup
- State Container: AppState provides dependencies to handlers
- Interface Segregation: Small, focused traits
- Inversion of Control: Handlers depend on abstractions, not implementations
- Data Access Abstraction: UrlDatabase trait
- Implementation Flexibility: Easy to swap database types
- Testing: Mock implementations for unit tests
- Commands:
insert_url()modifies state - Queries:
get_url()reads state - Clear Intent: Method names indicate read vs write operations
Test Strategy:
- In-Memory Database: SQLite
:memory:for fast, isolated tests - PostgreSQL Integration: Optional tests against real PostgreSQL instances
- Full Application: Tests complete request/response cycle
- Helper Functions: Shared test utilities and setup
- Tracing Integration: Optional logging for debugging
- Multi-Database: Tests run against multiple database backends
Test Structure:
tests/api/
├── main.rs # Test module declarations
├── helpers.rs # TestApp and spawn_app()
├── health_check.rs # Health endpoint tests
├── shorten.rs # URL shortening tests
└── redirect.rs # URL redirect tests
pub struct TestApp {
pub address: String,
pub client: reqwest::Client,
pub database: Arc<dyn UrlDatabase>,
pub api_key: Uuid,
}- Environment-Based: Different configs for local/production
- Environment Variables: Override any setting via
APP_*variables - Secrets Management: API keys and sensitive data via env vars
- File-Based: SQLite database stored as
database.db - Migrations: Automatic migration on startup
- Backup: Simple file backup/restore
- Health Checks:
/api/health_checkfor load balancer probes - Structured Logging: JSON logs for log aggregation
- Request Tracing: Correlation IDs for debugging
- Error Handling: Comprehensive error responses
The trait-based database abstraction currently supports multiple databases:
Implemented:
- SQLite:
SqliteUrlDatabaseinsrc/database/sqlite.rs - PostgreSQL:
PostgresUrlDatabaseinsrc/database/postgres_sql.rs
Adding New Databases:
pub struct MySqlUrlDatabase { /* ... */ }
impl UrlDatabase for MySqlUrlDatabase {
// Implement trait methods for MySQL
}Database Selection: Configured through environment settings, enabling easy switching between database backends.
Current API key middleware can be extended for:
- User-based authentication
- JWT tokens
- OAuth integration
- Rate limiting per user
The modular architecture supports adding:
- Custom short URL aliases
- URL expiration dates
- Analytics and click tracking
- User dashboards
- Bulk URL operations
Add caching layer between handlers and database:
- Redis for distributed caching
- In-memory cache for single instance
- Cache invalidation strategies
The project includes a comprehensive Nix development environment (flake.nix):
Features:
- Reproducible Environment: Consistent development setup across all platforms
- Rust Toolchain: Fenix-provided stable Rust with required components
- Dependencies: Pre-installed SQLx CLI, SQLite, and development tools
- LLVM Integration: Clang/LLD for optimized linking
- Pre-commit Hooks: Optional formatting and linting checks
Usage:
# Enter development environment
nix develop --accept-flake-config # --accept-flake-config is needed to accept the nix-community binary cache for faster builds.
# Automatic environment with direnv
echo "use flake . --accept-flake-config" > .envrc
direnv allowBenefits:
- Zero Setup: No manual dependency installation
- Version Consistency: Locked dependency versions across team
- CI/CD Integration: Same environment used in continuous integration
- Cross-Platform: Works on Linux, macOS, and Windows (WSL)
- Create handler in
src/lib/routes/ - Add route to router in
src/lib/startup.rs - Add integration tests in
tests/api/ - Update API documentation
- Create migration files in
migrations/(SQLite) andmigrations/pg/(PostgreSQL) - Update database trait if needed (
src/database/mod.rs) - Update implementations (
src/database/sqlite.rsandsrc/database/postgres_sql.rs) - Test migrations on both database backends
- Update configuration examples for new settings
- Update
Settingsstructs inconfiguration.rs - Add to relevant YAML files
- Update documentation
- Add validation if needed
This architecture provides a solid foundation for a URL shortener service while remaining extensible and maintainable. The clear separation of concerns and trait-based abstractions make it easy for contributors to understand and extend the system.