Part of the Tiny HTTP series.
One of the common ways style of web development employed by many we frameworks is the path-based routing.
Regardless of where you host your web application it will have URLs like these:
/
/newsletter
/about
/user/
/user/joe
/user/jane
...
If the request has parameters such as in this case:
/api/newsletter?name=Foo
we would disregard the parameters and use only
/api/newsletter
In order to make the code cleaner you could map each such path to a function and let that function handle the request.
So in the above case you will have a function to handle the request to /
, one to handle the request to /newsletter
and another one to handle the /about
.
For the pathes that start with /user/
you won't be able to implement one function each as there might be any number of such pathes. One for each user of your system.
So you will have a single function that can handle all the users.
In this example we'll see how to handle the fixed pathes (the first 3). Handling pathes in a more flexible way is left as an exercise to the reader. (and as TODO item for myself.)
Full example
examples/tiny-http/path-based-routing/src/main.rs
use ascii::AsciiString;
use std::str::FromStr;
use tiny_http::{Header, HeaderField, Response, Server};
fn main() {
let host = "127.0.0.1";
let port = "5000";
let server_str = format!("{}:{}", host, port);
let server = Server::http(&server_str).expect("Failed to start demo server.");
for request in server.incoming_requests() {
let path = if let Some((path, _)) = request.url().split_once('?') {
path
} else {
request.url()
};
println!("path: {}", path);
let html = match path {
"/" => root_page(&request),
"/hello" => hello(&request),
_ => default(&request),
};
let header = Header {
field: HeaderField::from_str("Content-type").unwrap(),
value: AsciiString::from_ascii("text/html").unwrap(),
};
request
.respond(Response::from_string(html).with_header(header))
.unwrap();
}
}
fn default(_request: &tiny_http::Request) -> String {
String::from("Page not found")
}
fn root_page(_request: &tiny_http::Request) -> String {
String::from(r#"Welcome! Try <a href="/hello">this</a> page."#)
}
fn hello(_request: &tiny_http::Request) -> String {
String::from(r#"Hello World! <a href="/">home</a> page."#)
}
Explanation
The first thing we need to do is to extract the path from the url. You might recall the return value of the url
method starts from the initial /
and does not include the hostname. We first try to split the string into two variables, but for the second variable
we use _
as we don't actually need that value.
If the split_once
fails then we know there is no ?
in the string so we can return the whole string.
let path = if let Some((path, _)) = request.url().split_once('?') {
path
} else {
request.url()
};
Then we use the match
pattern matching operator to implement the mapping. Each path is mapped to a function. Each function receives the request
object as a parameter.
The last one, mapping the underscore _
to a function called default
will be triggered if none of the earlier cases matched.
let html = match path {
"/" => root_page(&request),
"/hello" => hello(&request),
_ => default(&request),
};
The functions themselves are rather simple. In this simple case none of them make any use of the request
parameter, hence we prefixed them with an underscore.
How to try it?
cargo run
Will compile the code and run the web server.
- Visit
http://localhost:5000/
- Click on the link that will lead you to
http://localhost:5000/hello
- Click on the "home" link.
- Type in
http://localhost:5000/blabla
and you will see "Page not found".
Conclusion
- The
default
function should probably set the status code to 404 to indicate that the page not found - We only handle fixed pathes. We should be able to handle pathes in a more flexible way.
- We handle all the requests the same way regardless if they were a GET, POST, HEADER, etc. request.
I am sure there is a lot more to do, but this can already help make the code nicer.