Web experiments in Rust
Mon 29 December 2025
Short directions (Photo credit: Petr Kratochvil)
In python, I usually use Flask to quickly set up a simple webserver and/or API endpoint. I wanted to explore the same in rust.
Application
Requirements
I choose to implement a simple application. Yet, I don't want a stateless one so that I also learn how to keep and share state between calls.
I chose to implement a URL shortener.
Rust code
We need to store and retrieve full URL and to map it to a short URL. I chose to store the short url as an ID. The short URL is then https://{domain}/{ID}.
I need:
- a function to store a URL that return the ID
- a function to retrieve an URL given an ID
- a way to remove old (ID, URL).
Data structures
I create data structures for both URL and ID. This could seem overengineered, but the wrapping of a string in a dedicated structure allow for dedicated logic.
First the URL. When created from a string, several checks can be performed (URL begins with a (known) scheme, the correct number of colon...).
The implementation of use::str::FromStr ease parsing.
#[derive(Debug, Clone, Default)]
pub struct Url(String);
#[derive(Debug)]
pub enum URLParseError {
NoScheme,
InvalidScheme(String),
NoHost,
}
const KNOWN_SCHEME: [&str; 4] = ["http", "https", "ftp", "ftps"];
impl FromStr for Url {
type Err = URLParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut parts = s.split('/');
let scheme = match parts.next() {
Some(v) => v.to_lowercase(),
None => return Err(URLParseError::NoScheme),
};
if !scheme.ends_with(':') {
return Err(URLParseError::InvalidScheme(
"scheme should end with a ':'".into(),
));
}
if scheme.chars().filter(|&c| c == ':').count() != 1 {
return Err(URLParseError::InvalidScheme(
"scheme should not contain ':'".into(),
));
}
let scheme = scheme.split(':').next().unwrap();
if !KNOWN_SCHEME.contains(&scheme) {
return Err(URLParseError::InvalidScheme(format!(
"{} is not a known scheme",
&scheme
)));
}
let _ = parts.next();
let host = match parts.next() {
Some(v) => v.to_lowercase(),
None => return Err(URLParseError::NoHost),
};
let path = parts.collect::<Vec<_>>().join("/");
Ok(Self(format!("{scheme}://{host}/{path}")))
}
}
impl fmt::Display for Url {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
Then for the ID, the implementation is way simpler as the ID should be created by the new() method. The Hash trait is used to create a HashSet of ID, and effectively search for an ID in this set.
#[derive(Debug, Clone, Eq, PartialEq, Hash, Default)]
pub struct ID(String);
impl ID {
pub fn new() -> Self {
let mut rng = rand::rng();
let size = 5;
Self(
(0..size)
.map(|_| rng.sample(Alphanumeric) as char)
.collect(),
)
}
}
Then the struct containing the database. I used a VecDeque. The goal is to remove the oldest URLs. The URl are ordered in the VecDeque in chronological order, the last requested/recorded at the end of the VecDeque.
#[derive(Debug, Default)]
pub struct UrlSet {
set: VecDeque<(ID, Url)>,
max_size: usize,
}
Logic
When storing an URL, a valid available ID must be returned. When retrieving an URL, the (ID, URL) must then be reput in the VecDeque as a new URL. As usual, method to retrieve data can return None.
impl UrlSet {
pub fn store_url(&mut self, url: Url) -> ID {
if self.set.len() > self.max_size - 1 {
let _ = self.set.pop_front();
}
let ids = self.set.iter().map(|(elt, _)| elt).collect::<HashSet<_>>();
let mut id = ID::new();
while ids.contains(&id) {
eprintln!("regenrating id {}", id);
id = ID::new();
}
self.set.push_back((id.clone(), url));
id
}
/// Retrieve an url and put it back in queue
pub fn retrieve_refresh(&mut self, id: &ID) -> Option<Url> {
let p = self.set.iter().position(|(a, _)| a == id)?;
let (id, url) = self.set.remove(p)?;
self.set.push_back((id, url.clone()));
Some(url.clone())
}
}
Crate/framework choice
One of the most popular crate to create web server is rust is Axum. The official doc for this crate contains explicit examples.
For my short example, I use a shared state consisting of a UrlSet defined just above and the base URL.
We need two endpoints:
- an endpoint to store, that should be called by a PUT method and post the URL to shorten and return the short URL
- an endpoint to get the URL of a short URL, that should redirect to the long URL (or return an http error if the URL doesn't exist)
The shared state:
#[derive(Debug)]
struct AppState {
url_set: Mutex<UrlSet>,
base_url: String,
}
The function called by the post endpoint. It returns a simple response with status code whose body contains the shortened URL only.
#[derive(Deserialize, Debug)]
struct Input {
/// url to shorten
url: String,
}
/// return the shortened url
async fn shorten(
State(url_set): State<Arc<AppState>>,
Form(input): Form<Input>,
) -> impl IntoResponse {
let url: Url = match input.url.parse() {
Ok(v) => v,
Err(e) => {
return Response::builder()
.status(StatusCode::NOT_ACCEPTABLE)
.body(format!("{:?}", e))
.unwrap();
}
};
let base_url = url_set.base_url.clone();
let url_set = url_set.url_set.lock();
let id = url_set.await.store_url(url);
Response::new(format!("http://{}/{}", base_url, id))
}
The function called by the get method. It returns either a redirection or a response with a specified status code.
/// return redirection to url
async fn get_url(State(url_set): State<Arc<AppState>>, Path(id): Path<String>) -> Response {
let url_set = url_set.url_set.lock();
let url = url_set.await.retrieve_refresh(&id.parse().unwrap());
match url {
Some(v) => Redirect::to(format!("{v}").as_str()).into_response(),
None => (
StatusCode::NOT_FOUND,
format!("no shortcut register for {id}"),
)
.into_response(),
}
}
Those function are async to be used by axum.
And the routes added to the axum app, with the correct methods, and with the shared state. We must take care of types to make the compiler happy. In particular, mismatch type in shared state produce error messages I found unclear.
let app = Router::new()
.route("/", get(|| async { "Hello, World!" }))
.route("/{id}", get(get_url))
.route("/shorten", post(shorten))
.with_state(Arc::clone(&url_set));
Conclusion
Using axum to create a simple web application is almost as easy as using Flask in python. There are few attentions points, but those points are not suprising (types, async,...). The all code is available here.
Related articles (or not):