A high-performance URL shortener service built with modern Rust technologies. This service provides a simple API for creating shortened URLs and redirecting users to their original destinations.
- Fast URL shortening: Generate short, unique identifiers for long URLs using nanoid
- Reliable redirects: Permanent redirects to original URLs with proper HTTP status codes
- Rate limiting: Built-in rate limiting to prevent abuse using tower-governor
- Multi-database support: SQLite and PostgreSQL database backends
- Database abstraction: Trait-based database layer for easy database switching
- URL validation: Input validation with configurable URL length limits (2048 characters)
- Comprehensive logging: Structured logging with tracing and request IDs
- Health monitoring: Built-in health check endpoint
- API documentation: OpenAPI 3.0 specification with interactive Swagger UI
- Web interface: Admin panel with Tera templates
- API key protection: Secure API endpoints with UUID-based authentication
- Nix development environment: Flake-based dev environment with pre-commit hooks
- Production ready: Built for deployment with graceful shutdown handling
- Framework: Axum - Modern async web framework
- Databases: SQLite and PostgreSQL with SQLx for type-safe queries
- Rate Limiting: tower-governor - Per-IP rate limiting with GCRA algorithm
- Templates: Tera - Template engine for web interface
- Configuration: Figment - Layered configuration
- Logging: Structured logging with
tracingand Bunyan formatting - Development: Nix flake with Fenix Rust toolchain and pre-commit hooks
- Testing: Comprehensive integration tests with in-memory databases
This project uses cargo audit to check for security vulnerabilities. Due to unmaintained dependencies in the tera template engine (specifically unic crates), some warnings may appear. These are not security vulnerabilities but rather unmaintained crates.
To run the security audit with these warnings ignored, use the provided scripts:
# On Unix/Linux/macOS
./audit.sh
# On Windows
audit.batOr run manually with the ignore flags:
cargo audit --deny warnings \
--ignore RUSTSEC-2025-0081 \
--ignore RUSTSEC-2025-0075 \
--ignore RUSTSEC-2025-0080 \
--ignore RUSTSEC-2025-0074 \
--ignore RUSTSEC-2025-0104 \
--ignore RUSTSEC-2025-0098POST /api/shorten Content-Type: text/plain x-api-key: YOUR_API_KEY
Example curl -d 'https://www.google.com/' -H "x-api-key: e4125dd1-3d3e-43a1-bc9c-dc0ba12ad4b5" http://localhost:8000/api/shorten
Response: Returns a JSON response with shortened URL information
{ "success": true, "message": "ok", "status": 200, "time": "2025-10-05T12:00:00Z", "data": { "shortened_url": "http://localhost:8000/AbC123", "original_url": "https://www.google.com/", "id": "AbC123" } }
POST /api/public/shorten Content-Type: text/plain
Example curl -d 'https://www.example.com/' http://localhost:8000/api/public/shorten
Response: Same JSON format as authenticated endpoint, but may have stricter rate limits.
GET /api/redirect/{id}
Example curl -L http://localhost:8000/api/redirect/AbC123
Response: HTTP 308 Permanent Redirect to the original URL
GET /{id}
Example - Cleaner URL format curl -L http://localhost:8000/AbC123
Response: HTTP 308 Permanent Redirect to the original URL
Note: This is an alternative to /api/redirect/{id} for cleaner URLs.
GET /api/health_check
Example curl http://localhost:8000/api/health_check
Response: HTTP 200 OK with JSON envelope
{ "success": true, "message": "ok", "status": 200, "time": "2025-09-18T12:00:00Z", "data": null }
GET /admin
Example - View the web interface curl http://localhost:8000/admin
Response: HTML page with admin interface
Additional Admin Routes (all require API key):
GET /admin/profile- User profile managementGET /admin/login- Login pageGET /admin/register- Registration page
For complete route documentation, see ROUTE_ORGANIZATION.md.
The URL Shortener service provides comprehensive API documentation with OpenAPI 3.0 specification and interactive Swagger UI.
Visit the Swagger UI at: http://localhost:8000/api/docs
The interactive documentation provides:
- Complete API reference with all endpoints, parameters, and responses
- Interactive testing - Try out API calls directly from your browser
- Request/response examples for all endpoints
- Authentication support for protected endpoints
- Schema validation with automatic request/response validation
The OpenAPI 3.0 specification is available at: http://localhost:8000/api/docs/openapi.yaml
This YAML file can be used with:
- API clients like Postman, Insomnia, or REST Client
- Code generation tools to generate client SDKs
- Documentation tools for custom API documentation sites
- API testing tools for automated testing
- URL Shortening: Create short URLs with optional custom aliases
- URL Redirection: Fast redirects to original URLs
- Health Monitoring: Service health checks
- Authentication: API key-based authentication for protected endpoints
- Rate Limiting: Built-in rate limiting information
- Error Handling: Comprehensive error response documentation
- Rust (latest stable)
- SQLx CLI
- Database: SQLite (no setup required) or PostgreSQL (optional)
- Nix (optional, for Nix development environment)
- Clone the repository
git clone https://github.com/zero-to-mastery/url-shortener-ztm.git cd url-shortener-ztm
- Install dependencies
cargo build
- Create the Database
sqlx database create
- Run the application
cargo run
The database and migrations will be set up automatically on first run.
- Clone the repository
git clone https://github.com/zero-to-mastery/url-shortener-ztm.git cd url-shortener-ztm
- Enter the Nix development environment
nix develop --accept-flake-config # --accept-flake-config is needed to accept the nix-community binary cache for faster builds.
This provides a complete development environment with Rust toolchain, SQLx CLI, and all dependencies.
- Run the application
cargo run
- Test the service
Get your API key from configuration/base.yml (or set via environment) API_KEY="e4125dd1-3d3e-43a1-bc9c-dc0ba12ad4b5"
Shorten a URL curl -d 'https://example.com' -H "x-api-key: $API_KEY" http://localhost:8000/api/shorten
Visit the shortened URL curl -L http://localhost:8000/AbC123
Check health curl http://localhost:8000/api/health_check
Visit admin interface open http://localhost:8000/admin
This project uses just as a command runner for common development tasks. Think of it like make but simpler and more user-friendly.
Available Commands:
List all available commands just --list
Start in release mode (rate limiting ON, log level: warn) just start
Start in development mode (rate limiting OFF, log level: debug) just start-dev
Start with custom settings just start rate="false" log="info" just start-dev rate="true" log="trace"
Prepare test data just prepare-shorten-data just prepare-redirect-data
Run performance tests just perf-shorten # Test URL shortening performance just perf-redirect # Test redirect performance just perf-shorten-bench # Run benchmark suite
Installing Just:
macOS brew install just
Linux cargo install just
Windows scoop install just
Note: This project uses Nushell as the shell for just commands. Install Nushell from nushell.sh if you want to run performance tests.
For more information, visit the Just documentation.
The application supports environment-based configuration with YAML files:
configuration/base.yml- Base configuration (application, database, rate limiting)configuration/generator.yml- ID generator configuration (nanoid/sequence settings)configuration/local.yml- Local development overridesconfiguration/production.yml- Production settings
Set APP_ENVIRONMENT to local or production to load the appropriate config.
Override any setting using environment variables with APP_ prefix. Note: Use double underscores (__) to access nested configuration values:
Application settings APP_APPLICATION__PORT=3000 APP_APPLICATION__HOST=0.0.0.0 APP_APPLICATION__API_KEY=your-new-api-key
Database settings APP_DATABASE__TYPE=sqlite # or "postgres" APP_DATABASE__URL=sqlite:database.db APP_DATABASE__CREATE_IF_MISSING=true
Rate limiting APP_RATE_LIMITING__ENABLED=false APP_RATE_LIMITING__REQUESTS_PER_SECOND=100 APP_RATE_LIMITING__BURST_SIZE=20
Configuration Hierarchy:
APP_prefix indicates environment variable- Double underscore (
__) separates nested YAML keys - Example:
APP_DATABASE__URLmaps todatabase.urlin YAML - Example:
APP_APPLICATION__API_KEYmaps toapplication.api_keyin YAML
Generator Configuration:
The configuration/generator.yml file controls ID generation behavior:
shortener: length: 7 # Length of generated short codes alphabet: "0-9A-Za-z" # Characters used in short codes engine: kind: "nanoid" # Generator type: "nanoid" or "sequence"
Override via environment: APP_SHORTENER__LENGTH=8 APP_SHORTENER__ENGINE__KIND=sequence
The service protects write endpoints with a UUID-based API key.
- The base config includes an obviously insecure development key so
cargo runworks out of the box. - On startup, the app detects this default key and prints a prominent warning to the console.
- In any non-local environment, you MUST override the key via environment variable.
Generate a UUID v4:
Linux/macOS uuidgen
PowerShell Rust (optional helper): cargo run --bin print-uuid
Set the key via env var:
APP_APPLICATION__API_KEY=$(uuidgen)
Production guidance:
- Store secrets in your platform's secret manager (e.g., Fly.io, Railway, Kubernetes, GitHub Actions).
- Rotate keys when compromised or on developer offboarding.
- Never commit real keys to version control.
SQLite Configuration (Default)
database: type: sqlite url: "sqlite:database.db" # Path to SQLite database file create_if_missing: true # Create database if it doesn't exist max_connections: 16 # optional set database pool connection min_connections: 4 # optional set database pool connection
PostgreSQL Configuration
database: type: postgres host: "localhost" port: 5432 username: "app" password: "secret" database_name: "urlshortener" max_connections: 64 # optional set database pool connection min_connections: 16 # optional set database pool connection create_if_missing: true
For in-memory database (testing):
database: type: sqlite url: ":memory:" create_if_missing: true
The service includes built-in rate limiting to prevent abuse using the tower-governor crate:
rate_limiting: enabled: true # Enable/disable rate limiting requests_per_second: 10 # Maximum sustained request rate per IP burst_size: 5 # Additional burst capacity per IP
Environment-specific examples:
Development (configuration/local.yml):
rate_limiting: enabled: true requests_per_second: 20 # More lenient for development burst_size: 10
Production (configuration/production.yml):
rate_limiting: enabled: true requests_per_second: 5 # Strict rate limiting for production burst_size: 3
Rate Limiting Behavior:
- Limits are applied per IP address using the GCRA (Generic Cell Rate Algorithm)
- Only URL shortening endpoints are rate limited (
/api/shorten,/api/public/shorten) - Health checks and redirects are not rate limited
- Standard HTTP headers are included in rate limit responses:
retry-after: Seconds to wait before retryingx-ratelimit-after: Additional rate limiting information
- Returns HTTP 429 Too Many Requests when limits are exceeded
Environment Variable Override:
APP_RATE_LIMITING__ENABLED=false # Disable rate limiting APP_RATE_LIMITING__REQUESTS_PER_SECOND=100 # 100 requests per second APP_RATE_LIMITING__BURST_SIZE=20 # Allow bursts of 20 requests
The project includes comprehensive integration tests using in-memory databases.
Run all tests cargo test
Run tests with logging output TEST_LOG=1 cargo test
Run specific test module cargo test health_check cargo test redirect cargo test shorten
Run PostgreSQL tests (requires running PostgreSQL) cargo test postgres_database_insert_get -- --ignored
- β Health check endpoint with JSON envelope validation
- β URL shortening functionality with API key authentication
- β URL redirection with proper HTTP status codes
- β URL length validation (2048 character limit)
- β Rate limiting with per-IP enforcement and proper HTTP headers
- β SQLite database integration with trait abstraction
- β PostgreSQL database integration (optional)
- β Error handling and edge cases
src/ βββ bin/ β βββ main.rs # Application entry point βββ lib.rs # Library crate root βββ configuration.rs # Configuration management βββ errors.rs # Error types and handling βββ middleware.rs # API key authentication βββ response.rs # JSON response envelope βββ startup.rs # Application startup and router βββ state.rs # Application state management βββ telemetry.rs # Logging and tracing setup βββ templates.rs # Template rendering βββ database/ β βββ mod.rs # Database trait definitions β βββ sqlite.rs # SQLite implementation β βββ postgres_sql.rs # PostgreSQL implementation βββ generator/ β βββ mod.rs # ID generator module β βββ config.rs # Generator configuration β βββ nanoid.rs # Nanoid generator implementation β βββ sequence.rs # Sequential ID generator βββ models/ β βββ mod.rs # Data models βββ shortcode/ β βββ mod.rs # Short code management β βββ bloom_filter.rs # Bloom filter for collision detection βββ routes/ βββ mod.rs # Route module exports βββ health_check.rs # Health check handler βββ index.rs # Index page handler βββ admin.rs # Admin interface handler βββ docs.rs # API documentation (Swagger/OpenAPI) βββ shorten.rs # URL shortening handler βββ redirect.rs # URL redirect handler
tests/ βββ api/ β βββ main.rs # Integration test entry β βββ helpers.rs # Test utilities and setup β βββ health_check.rs # Health check tests β βββ shorten.rs # URL shortening tests β βββ redirect.rs # URL redirect tests β βββ rate_limiting.rs # Rate limiting tests β βββ error_handling.rs # Error handling tests β βββ alias_validation_consistency.rs # Alias validation tests β βββ static_assets.rs # Static asset serving tests βββ perf/ βββ shorten.js # Performance tests for shortening βββ redirect.js # Performance tests for redirects βββ shortener-bench.js # Benchmark suite βββ run_shortener-bench.nu # Benchmark runner script
configuration/ βββ base.yml # Base configuration βββ generator.yml # ID generator configuration βββ local.yml # Local development config βββ production.yml # Production config
migrations/ βββ 20251017163705_url_shortener_ztm.up.sql # SQLite schema βββ 20251017163705_url_shortener_ztm.down.sql # SQLite rollback βββ 20251017184220_add_users_and_sessions.up.sql # SQLite users/sessions βββ 20251017184220_add_users_and_sessions.down.sql # SQLite users/sessions rollback βββ 20251107120000_add_bloom_snapshots_table.up.sql # SQLite bloom filter snapshots βββ 20251107120000_add_bloom_snapshots_table.down.sql # SQLite bloom filter rollback βββ pg/ # PostgreSQL migrations βββ 20251015003911_url_shortener_ztm_pg.up.sql # PostgreSQL schema βββ 20251015003911_url_shortener_ztm_pg.down.sql # PostgreSQL rollback βββ 20251015102402_init_url_shortener.up.sql # PostgreSQL initialization βββ 20251015102402_init_url_shortener.down.sql # PostgreSQL init rollback βββ 20251107120000_add_bloom_snapshots_table.up.sql # PostgreSQL bloom filter βββ 20251107120000_add_bloom_snapshots_table.down.sql # PostgreSQL bloom rollback
scripts/ # Utility scripts (Nushell) βββ gen_repeat_url.nu # Generate repeated URLs for testing βββ gen_req_url.nu # Generate request URLs βββ gen_short_id.nu # Generate short IDs βββ get_ulr_data_from_db.nu # Query URL data from database βββ get_urls.nu # Fetch URLs βββ helpers.nu # Helper functions βββ prepare_redirect_data.nu # Prepare redirect test data βββ prepare_shorten_data.nu # Prepare shorten test data
static/ # Static web assets βββ screen.css # CSS styles βββ scripts.js # JavaScript
templates/ # Tera templates βββ base.html # Base template βββ index.html # Index page βββ admin.html # Admin interface βββ login.html # Login page βββ profile.html # User profile page βββ register.html # Registration page
docs/ βββ deployment-guide.md # Deployment documentation
justfile # Just command runner recipes openapi.yaml # OpenAPI 3.0 specification flake.nix # Nix development environment flake.lock # Nix lock file
For detailed route organization, see ROUTE_ORGANIZATION.md.
The application uses a trait-based database abstraction (UrlDatabase) that supports both SQLite and PostgreSQL:
#[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>; }
Comprehensive error handling with custom ApiError types and structured JSON responses:
pub enum ApiError { BadRequest(String), NotFound(String), Unauthorized(String), Internal(String), // ... }
Layered configuration system supporting YAML files and environment variables with automatic environment detection.
CREATE TABLE urls ( id INTEGER PRIMARY KEY, code TEXT NOT NULL UNIQUE, -- Short identifier (nanoid, 7 characters) url TEXT NOT NULL, -- Original URL url_hash BLOB NOT NULL UNIQUE -- SHA-256 hash for deduplication );
CREATE TABLE aliases ( alias TEXT PRIMARY KEY, -- Custom alias for a URL target_id INTEGER NOT NULL REFERENCES urls(id) ON DELETE CASCADE );
CREATE INDEX aliases_target_id_idx ON aliases(target_id);
CREATE VIEW all_short_codes AS SELECT u.code AS code, u.id AS target_id, u.url AS url, 'code' AS source FROM urls u UNION ALL SELECT a.alias AS code, a.target_id, u.url, 'alias' AS source FROM aliases a JOIN urls u ON u.id = a.target_id;
This view combines primary codes and aliases for unified lookups.
CREATE TABLE bloom_snapshots ( name TEXT PRIMARY KEY, data BLOB NOT NULL, -- Serialized bloom filter updated_at TEXT NOT NULL DEFAULT (datetime('now')) );
url_hash- SHA-256 hash enables deduplication of identical URLsaliases- Supports custom short code aliases for memorable URLsall_short_codes- View provides unified access to both codes and aliasesbloom_snapshots- Persists bloom filter for fast collision detection across restarts- Triggers - Database triggers prevent conflicts between codes and aliases
- Structured Logging: JSON-formatted logs with request correlation IDs
- Request Tracing: Full request lifecycle tracing with
tracingcrate - Health Checks:
/api/health_checkendpoint with JSON envelope response - Error Handling: Comprehensive error responses with appropriate HTTP status codes
- Request IDs: Automatic request ID generation and propagation
- API Key Authentication: Protected endpoints require valid UUID-based API keys
- Input Validation: URL parsing and length validation before storage
- SQL Injection Protection: Type-safe queries with SQLx
- Error Information Disclosure: Sanitized error responses
- Resource Protection: URL length limits prevent resource exhaustion attacks
- SQLite database support with migrations
- Database abstraction layer
- Web UI with Tera templates
- API key authentication
- Comprehensive error handling
- Integration tests
- PostgreSQL database support
- Rate limiting with tower-governor
- URL length validation (2048 characters)
- Nix development environment with flake
- User authentication and URL management
- Analytics and usage statistics
- Custom short URL aliases
- URL expiration and cleanup
- Docker containerization
- Real-world API specification compliance
Contributions are welcome! Please feel free to submit a Pull Request.
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
- Ensure all tests pass (
cargo test) - Follow Rust naming conventions
- Add tests for new functionality
- Update documentation as needed
This project is licensed under the MIT License - see the License.txt file for details.
Jeffery D. Mitchell
- Email: [email protected]
- GitHub: @crustyrustacean
- Built with the excellent Rust web ecosystem
- Inspired by modern web service architecture patterns
- Thanks to the Rust community for amazing tools and libraries
You can deploy this project to various platforms like Railway, Fly.io, and DigitalOcean.
π Check the full Deployment Guide for detailed instructions.