Simple URL shortening service written in Rust. The service provides a basic API allowing clients to pass a long URL and receive a shortened version for easy access and sharing.
Usage: rust-short-url.exe [OPTIONS]
Options:
-m, --mode <MODE>
Mode this service will be running in [possible values: in-memory, persistent, coordinator]
-a, --application-url <APPLICATION_URL>
Address this service will be running on
--hostnames <HOSTNAMES>
List of host names separated by space to coordinate requests between
-h, --help
Print help
Application settings can be configured in development.toml
or production.toml
files. Possible configurations include mode of the application, API server configuration, database connection, rate limiting and coordinator options.
The HashService
trait represents a service responsible for managing urls operations, such as initialization, retrieving links, inserting values, and finding information associated with a key.
HTTP server exposes several endpoints for the clients, such as:
- GET /urls - get all urls
- GET /shorten?long_url{url} - shorten given long url
- GET /{short_url} - redirect to url behind this shortened version
- GET /{short_url}/summary - get summary information about provided short url
The application can be launched in coordinator mode, coexisting with multiple regular instances within the same deployment. To enable coordination, a list of hostnames for these instances must be provided. In this mode, the application constructs a hash ring that encompasses all instances. Upon receiving a request, it forwards the request to all other machines, awaits acknowledgments from each, and then returns a response.
From the client's perspective, it appears as a single application. However, the underlying data is replicated across all instances. This replication enables fine-grained control over the consistency and availability of the data. When considering N
as the number of replicas, W
as the write quorum (considered successful if acknowledged by W replicas), and R
as the read quorum (considered successful if acknowledged by R replicas), the following scenarios arise:
- If
R = 1
andW = N
, the system is optimized for fast reads. - If
W = 1
andR = N
, the system is optimized for fast writes. - If
W + R > N
, strong consistency is guaranteed (typically withN = 3
,W = R = 2
). - If
W + R <= N
, strong consistency is not guaranteed.
To run the service in development mode, use the following command:
cargo run
By default, the development.toml
configuration is used, and RUN_MODE='development'
is set.
Follow these steps to run the service in production:
- Configure MongoDB connection in
production.toml
. - Set the environment variable
RUN_MODE
to 'production':
$env:RUN_MODE='production'
- Execute the following command:
cargo run
To run the service using Docker Compose, use the following command:
docker-compose up
This command launches the service along with its dependencies defined in the docker-compose.yml file.
To run multiple instances, use the following command:
00_start.ps1
Example output:
Instance 1 started on port 63975
Instance 2 started on port 63976
Instance 3 started on port 63977
Coordinator started on port 80
This application supports integration with Google Firestore when the configuration parameter Mode=Firestore
is set. To enable Firestore access, you need to set up application credentials with Google Cloud Platform (GCP). Follow these steps to configure and run the application:
-
Create a Service Account in GCP
- Navigate to the Google Cloud Console.
- Go to IAM & Admin > Service Accounts.
- Click Create Service Account, provide a name and description, and proceed.
- Grant the service account the necessary permissions for Firestore (e.g., Cloud Datastore User role).
- Complete the creation process.
-
Generate and Download a JSON Key
- In the Service Accounts list, locate your newly created service account.
- Click on the service account, go to the Keys tab, and select Add Key > Create New Key.
- Choose JSON format, then download and securely store the file (e.g.,
urlshortener-445813-7cb90fbf70f8.json
).
-
Save the Key File Locally
- Save the downloaded key file as
service-account.json
in a secure directory, e.g.,secrets
. - Add
secrets/service-account.json
to your.gitignore
to prevent it from being included in version control.
- Save the downloaded key file as
-
Map the JSON File in Docker Compose
-
Modify your
compose.yml
to include the following configuration:volumes: # Map the JSON file from your local system to the container - ./secrets/service-account.json:/secrets/service-account.json:ro environment: - RUN_MODE=production - GOOGLE_APPLICATION_CREDENTIALS=/secrets/service-account.json
-
-
Set the Application Credentials
- In your terminal, set the
GOOGLE_APPLICATION_CREDENTIALS
environment variable to the path of your JSON key file:For other shells (e.g., bash):$env:GOOGLE_APPLICATION_CREDENTIALS="D:\Downloads\urlshortener-445813-7cb90fbf70f8.json"
export GOOGLE_APPLICATION_CREDENTIALS="/path/to/urlshortener-445813-7cb90fbf70f8.json"
- In your terminal, set the
-
Test locally
- Run the application using Cargo:
cargo run
- Run the application using Cargo:
-
Deploy the application
bash docker-compose -f compose.yaml up -d --build
Execute a POST request to shorten a URL. Example:
curl -X GET "http://localhost/shorten?long_url=https://doc.rust-lang.org/"
The response will contain a short URL, e.g., localhost/1C96D51A
.
You can now use localhost/1C96D51A
as the shortened URL.
This project demonstrates a cloud-based URL shortening service with an Angular frontend and Rust backend service deployed on Google Cloud Run. The architecture uses Cloudflare for frontend hosting, domains, routing and redirects. Below is an explanation of the deployment workflow and user interaction:
-
User Interaction with Angular App:
- When a user visits
https://surl.ivanenkomak.com/
, they are served the Angular frontend hosted behind Cloudflare. - The Angular app allows users to input a Long URL and click "Shorten" to generate a shortened URL.
- When a user visits
-
Shortened URL Generation:
- The Angular app generates and displays a Short URL, such as
https://surl.ivanenkomak.com/9032D81A
. - This Short URL includes a unique identifier (
9032D81A
) that maps to the original Long URL.
- The Angular app generates and displays a Short URL, such as
-
Redirect via Cloudflare:
- When a user visits a Short URL
https://surl.ivanenkomak.com/
, Cloudflare applies routing rules to differentiate between requests:- Requests to
/
are served the Angular app. - Requests to
/*
(paths with at least one character) are forwarded to the backend service.
- Requests to
- When a user visits a Short URL
-
Backend Service on Google Cloud Run:
- The backend service, deployed on Google Cloud Run, receives the request with the unique identifier (
9032D81A
). - It resolves the identifier to the original Long URL and issues a redirect to the user.
- The backend service, deployed on Google Cloud Run, receives the request with the unique identifier (
The following diagram illustrates the architecture and workflow:
The following Cloudflare routing rules are used to handle traffic:
- Root Path (
/
):- Requests to the root path serve the Angular app (
index.html
).
- Requests to the root path serve the Angular app (
- Short URLs (
/*
):- Requests to any path with a unique identifier are forwarded to the backend service hosted on Google Cloud Run.
-
Long URL:
https://github.com/ivanenkomaksym/rust-short-url/blob/master/README.md
-
Short URL:
https://surl.ivanenkomak.com/9032D81A
-
Redirection:
- When the user visits the Short URL (
https://surl.ivanenkomak.com/9032D81A
), Cloudflare forwards the request to the backend. - The backend resolves the identifier (
9032D81A
) to the Long URL and redirects the user tohttps://github.com/ivanenkomaksym/rust-short-url/blob/master/README.md
.
- When the user visits the Short URL (
Alex Xu, System Design Interview – An insider's guide, 2020