A serverless URL shortener built with Rust and AWS Lambda, featuring automatic metadata extraction and click tracking.
- URL Shortening: Generate short, unique links using CUID2
- Metadata Extraction: Automatically fetches title, description, and content type from target URLs
- Click Tracking: Increments click counter on each redirect
- List URLs: Paginated listing of all shortened URLs with metadata
- Serverless Architecture: Runs on AWS Lambda with ARM64 architecture for cost efficiency
The application follows a trait-based dependency injection pattern for testability and maintainability.
graph TB
Client[Client] --> APIGW[API Gateway HTTP API]
APIGW --> Handler[Lambda Handler]
Handler --> Core[UrlShortener Core Logic]
Core --> UrlRepo[UrlRepository Trait]
Core --> UrlInfo[UrlInfo Trait]
UrlRepo --> DynamoDB[DynamoDbUrlRepository]
UrlInfo --> HTTP[HttpUrlInfo]
DynamoDB --> DB[(DynamoDB Table)]
HTTP --> Internet[Internet]
style Core fill:#90EE90
style UrlRepo fill:#FFE4B5
style UrlInfo fill:#FFE4B5
style DynamoDB fill:#527FFF
style HTTP fill:#87CEEB
style Handler fill:#FF9900
├── shared/ # Shared business logic
│ ├── src/
│ │ ├── core.rs # Business logic + traits (UrlRepository, UrlInfo)
│ │ ├── adapters.rs # DynamoDbUrlRepository implementation
│ │ ├── url_info.rs # HttpUrlInfo implementation
│ │ └── utils.rs # HTTP response helpers
├── lambdas/
│ ├── create_link/ # POST /links handler
│ ├── get_links/ # GET /links handler
│ └── visit_link/ # GET /{linkId} handler
└── integration-tests/ # End-to-end tests
- Trait Abstraction:
UrlRepositoryandUrlInfotraits decouple business logic from infrastructure - Dependency Injection: Concrete implementations (
DynamoDbUrlRepository,HttpUrlInfo) injected at runtime - Generic Core:
UrlShortener<R: UrlRepository, I: UrlInfo>works with any implementation - Adapter Pattern: Infrastructure adapters implement trait interfaces
- AWS Lambda: Rust functions running on
provided.al2023runtime with ARM64 architecture - Amazon DynamoDB: NoSQL database for storing shortened URLs (PAY_PER_REQUEST billing)
- Amazon API Gateway (HTTP API): RESTful API endpoints
| Method | Path | Description |
|---|---|---|
| POST | /links |
Create a new shortened URL |
| GET | /links |
List all shortened URLs (supports pagination) |
| GET | /{linkId} |
Redirect to original URL and increment clicks |
Each shortened URL stores:
LinkId(Primary Key): Unique identifier generated with CUID2OriginalLink: The target URLClicks: Number of times the link has been accessedTitle: Extracted page title (optional)Description: Extracted meta description (optional)ContentType: HTTP content type (optional)
- Rust
- AWS SAM CLI
- Cargo Lambda
- AWS Account with appropriate credentials configured
git clone https://github.com/ramtoearth/serverless-link-shortener.git
cd serverless-link-shortenersam build --beta-featuresThe project uses a trait-based architecture with the mockall crate for comprehensive unit testing without external dependencies.
Run all tests:
cargo testTest specific lambda:
cd lambdas/create_link
cargo testThe application separates concerns into three layers:
-
Core Business Logic (
shared/src/core.rs)- Pure business logic with trait abstractions
UrlRepositorytrait for data persistence operationsUrlInfotrait for URL metadata fetching- Fully testable with mocks, no AWS/network calls required
-
Infrastructure Adapters (
shared/src/adapters.rs,shared/src/url_info.rs)DynamoDbUrlRepository: Real DynamoDB implementationHttpUrlInfo: Real HTTP metadata scraping- Used in production, swapped with mocks in tests
-
Lambda Handlers (
lambdas/*/src/http_handler.rs)- Generic over trait implementations:
function_handler<R: UrlRepository, I: UrlInfo> - Tests use mocks, production uses real adapters
- Validates HTTP request/response handling
- Generic over trait implementations:
Example test with mocks:
#[tokio::test]
async fn test_shorten_url() {
let mut mock_repo = MockUrlRepository::new();
mock_repo.expect_store_short_url()
.times(1)
.returning(|url, id, details| Ok(ShortUrl::new(id, url, 0, details.title, details.description, details.content_type)));
let mut mock_info = MockUrlInfo::new();
mock_info.expect_fetch_details()
.returning(|_| Ok(UrlDetails { title: Some("Test".to_string()), ..Default::default() }));
let shortener = UrlShortener::new(mock_repo, mock_info);
let result = shortener.shorten_url(request).await;
assert!(result.is_ok());
}Benefits:
- Fast tests (no AWS credentials or network calls)
- Test error scenarios easily
- Zero production overhead (mocks compiled out in release builds)
For detailed testing architecture, see TESTING_ARCHITECTURE.md.
Deploy to AWS:
sam deploy --guidedOn first deployment, you'll be prompted to configure:
- Stack name (e.g.,
shortener-link) - AWS Region
- Confirmation prompts
- Capabilities (IAM role creation)
Subsequent deployments can use:
sam deployAfter deployment, view Lambda function logs in real-time:
sam logs --stack-name shortener-link --name ShortenUrlFunction -tThe -t flag tails the logs, showing new entries as they arrive.
After deployment, SAM will output your API endpoint URL. Use it to interact with the service:
curl -X POST {api_url}/links \
-H "Content-Type: application/json" \
-d '{"url_to_shorten": "https://example.com/very/long/url"}'curl {api_url}/linkscurl -L {api_url}/{linkId}This will redirect to the original URL and increment the click counter.
The Lambda function uses the following environment variable:
TABLE_NAME: DynamoDB table name (automatically set by SAM)
To delete all AWS resources:
sam deleteNote: The DynamoDB table has DeletionPolicy: Retain to prevent accidental data loss. Delete it manually from the AWS Console if needed.