A router matches requests against registered routes and invokes the associated request handler to return a response. There are many routers available, but they are often tied to a whole framework. Instead, this article explains how to handle HTTP requests with Hyper, how to route requests with Rust pattern matching, and how to handle query parameters, forms, cookies, and more. It assumes you are familiar with the basics of Linux, HTTP, and Rust. For full code samples, see the source files.
§Introduction
The first step is to create a minimal HTTP server that you can build on for the rest of this article:
$ cargo new --bin web
     Created binary (application) `web` package
	Add the crates cookie, form_urlencoded, hyper and tokio as
dependencies (enable the full feature set so you can experiment freely):
[package]
name = "web"
version = "0.1.0"
edition = "2018"
[dependencies]
cookie = "*"
form_urlencoded = "*"
hyper = { version = "*", features = ["full"] }
tokio = { version = "*", features = ["full"] }
	Import the required items:
use std::convert::Infallible;
use std::net::SocketAddr;
use hyper::service::{make_service_fn, service_fn};
use hyper::{Body, Request, Response, Server, StatusCode};
	The following function builds a successful Response with the status 200 OK and an empty Body:
fn ok() -> Response<Body> {
    Response::builder()
        .status(StatusCode::OK)
        .body(Body::empty())
        .unwrap()
}
	A handler is a function that takes a Request and returns a Response.
Reuse the previous function to create handle, the primary request handler:
fn handle(_req: Request<Body>) -> Response<Body> {
    ok()
}
	The main function instantiates an HTTP server listening on 127.0.0.1:3000
which invokes handle for each incoming request:
#[tokio::main]
async fn main() {
    let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
    eprintln!("Listening on {}", addr);
    // The closure passed to `make_service_fn` is executed each time a new
    // connection is established and returns a future that resolves to a
    // service.
    let make_service = make_service_fn(|_conn| async {
        // The closure passed to `service_fn` is executed each time a request
        // arrives on the connection and returns a future that resolves
        // to a response.
        Ok::<_, Infallible>(service_fn(|req| async {
            // Call the request handler.
            Ok::<_, Infallible>(handle(req))
        }))
    });
    // Start the server.
    if let Err(e) = Server::bind(&addr).serve(make_service).await {
        eprintln!("Error: {:#}", e);
        std::process::exit(1);
    }
}
	You can start this server using Cargo:
$ cargo run --bin 000-introduction
Listening on 127.0.0.1:3000
	Then, query it with cURL (the -i option includes the response headers in the
output):
$ curl -i http://localhost:3000/
HTTP/1.1 200 OK
content-length: 0
date: Thu, 1 Jan 1970 00:00:00 GMT
	§HTTP
The target of an HTTP request corresponds to a resource (e.g., a list of tasks
for /tasks, the first one for /tasks/1). A method defines an action on a
particular resource (e.g., POST /tasks to create a new task, GET /tasks/1
to view the first one). When a resource does not exist, or the method for that
resource is not allowed, the server returns an error to the client.
§Resources
If there is no request handler associated with a resource, the server returns
404 Not Found:
fn not_found() -> Response<Body> {
    Response::builder()
        .status(StatusCode::NOT_FOUND)
        .body(Body::empty())
        .unwrap()
}
	Create an handler index that always responds with 200 OK:
fn index(_req: &Request<Body>) -> Response<Body> {
    ok()
}
	Update handle to route requests for / to index, or return 404 Not Found
for any other target:
fn handle(req: Request<Body>) -> Response<Body> {
    match req.uri().path() {
        "/" => index(&req),
        _ => not_found(),
    }
}
	A request for / returns 200 OK as expected:
$ curl -i http://localhost:3000/
HTTP/1.1 200 OK
content-length: 0
date: Thu, 1 Jan 1970 00:00:00 GMT
	For any other target, the server returns 404 Not Found:
$ curl -i http://localhost:3000/foo
HTTP/1.1 404 Not Found
content-length: 0
date: Thu, 1 Jan 1970 00:00:00 GMT
	§Methods
The method defines the semantics of a request for a particular resource. A handler associated with a resource receives all the requests for this target, regardless of the method. Therefore, it is the responsibility of the handler to allow or deny specific methods, but as an additional guard, the server can deny methods it does not implement before calling any handler.
§Method not allowed
A handler receiving a request with a forbidden method returns 405 Method Not Allowed with the Allow header to indicate the list of valid methods for that
resource:
fn method_not_allowed<S: AsRef<str>>(methods: S) -> Response<Body> {
    Response::builder()
        .status(StatusCode::METHOD_NOT_ALLOWED)
        .header(header::ALLOW, methods.as_ref())
        .body(Body::empty())
        .unwrap()
}
	Update index to restrict methods other than GET and HEAD:
fn index(req: &Request<Body>) -> Response<Body> {
    if !matches!(req.method(), &Method::GET | &Method::HEAD) {
        return method_not_allowed("GET, HEAD");
    }
    ok()
}
	A POST request for / returns 405 Method Not Allowed:
$ curl -i -X POST http://localhost:3000/
HTTP/1.1 405 Method Not Allowed
content-length: 0
date: Thu, 1 Jan 1970 00:00:00 GMT
	§Not implemented
When a method is not allowed in any of the request handlers, the server can
return 501 Not Implemented:
fn not_implemented() -> Response<Body> {
    Response::builder()
        .status(StatusCode::NOT_IMPLEMENTED)
        .body(Body::empty())
        .unwrap()
}
	Since index only accepts GET or HEAD, you can update handle to return
this status for any other method:
fn handle(req: Request<Body>) -> Response<Body> {
    if !matches!(req.method(), &Method::GET | &Method::HEAD) {
        return not_implemented();
    }
    match req.uri().path() {
        "/" => index(&req),
        _ => not_found(),
    }
}
	With the method POST, the server returns 501 Not Implemented:
$ curl -i -X POST http://localhost:3000/
HTTP/1.1 501 Not Implemented
content-length: 0
date: Thu, 1 Jan 1970 00:00:00 GMT
	§Errors
When the target is unknown, the server returns 404 Not Found, which is an
example of a client error. On the contrary, being unable to connect to the
database is an example of server error, and it returns 500 Internal Server Error to the client:
fn internal_server_error() -> Response<Body> {
    Response::builder()
        .status(StatusCode::INTERNAL_SERVER_ERROR)
        .body(Body::empty())
        .unwrap()
}
	In a resource handler, you may have to parse request parameters, query a
database, render templates, all of which can produce errors. Therefore, the
request handlers return a Result<T, E>, where T is a Response. Client
errors are regular responses with the Ok variant, but server errors use the
Err variant.
Define a generic Result<T> type for all the handlers (you can also use a type
defined in crates such as anyhow):
type Result<T> = std::result::Result<T, Box<dyn std::error::Error + 'static>>;
	Then, update index to return this type:
fn index(req: &Request<Body>) -> Result<Response<Body>> {
    if !matches!(req.method(), &Method::GET | &Method::HEAD) {
        return Ok(method_not_allowed("GET, HEAD"));
    }
    Ok(ok())
}
	Create the handler route, responsible for routing the requests as handle
did previously, but instead returning a Result:
fn route(req: &Request<Body>) -> Result<Response<Body>> {
    match req.uri().path() {
        "/" => index(req),
        "/error" => Err("This is an error".into()),
        _ => Ok(not_found()),
    }
}
	handle has to return a Response for each request, so any error from route
needs to be turned into a 500 Internal Server Error response:
fn handle(req: Request<Body>) -> Response<Body> {
    route(&req).unwrap_or_else(|err| {
        eprintln!("Error: {:#}", err);
        internal_server_error()
    })
}
	A request for / returns 200 OK:
$ curl -i http://localhost:3000/
HTTP/1.1 200 OK
content-length: 0
date: Thu, 1 Jan 1970 00:00:00 GMT
	A request for /error returns 500 Internal Server Error.
$ curl -i http://localhost:3000/error
HTTP/1.1 500 Internal Server Error
content-length: 0
date: Thu, 1 Jan 1970 00:00:00 GMT
	The error message is written to the console:
$ cargo run --bin 130-errors
Listening on 127.0.0.1:3000
Error: This is an error
	§Macros
You may have to repeat some operations in multiple handlers. 405 Method Not Allowed responses must have an Allow header, so you also have to return the
list of methods in the pattern (unless you can tolerate a slight deviation from
RFC7231).
To reduce code duplication and automatically generate the list of allowed methods, you can create a declarative macro:
macro_rules! allow_method {
    ($l:expr, $($r:pat)|+) => {{
        use hyper::Method;
        if !matches!($l, $($r)|+) {
            let mut allowed = Vec::new();
            for method in &[
                Method::GET,
                Method::HEAD,
                Method::POST,
                Method::PUT,
                Method::PATCH,
                Method::DELETE,
            ] {
                if matches!(method, $($r)|+) {
                    allowed.push(method.as_str());
                }
            }
            return Ok(self::method_not_allowed(allowed.join(", ")));
        }
    }};
}
	Update index to make use of it:
fn index(req: &Request<Body>) -> Result<Response<Body>> {
    allow_method!(req.method(), &Method::GET | &Method::HEAD);
    Ok(ok())
}
	With the method POST for /, the server returns 405 Method Not Allowed
including the list of allowed methods:
$ curl -i -X POST http://localhost:3000/
HTTP/1.1 405 Method Not Allowed
allow: GET, HEAD
content-length: 0
date: Thu, 1 Jan 1970 00:00:00 GMT
	Procedural macros may provide a more efficient implementation, which I leave as an exercise for the reader...
§Routing
A route associates an HTTP target with a resource. Up until now, route relies
on a naive string comparison with the target. By leveraging Rust pattern
matching, it is possible to match and extract parameters from dynamic routes.
§Path segmentation
A URL is formed of / separated components. A simple way to match a URL is to
split it into segments. For instance, /hello/world is "equivalent" to the
segments ["hello", "world"].
First, add a new function to build a 200 OK response with an UTF-8 encoded
plain text body:
fn ok_with_text<S: Into<String>>(text: S) -> Response<Body> {
    Response::builder()
        .status(StatusCode::OK)
        .header(header::CONTENT_TYPE, "text/plain; charset=utf-8")
        .body(Body::from(text.into()))
        .unwrap()
}
	Add the handler hello, that returns a custom "Hello, World!" based on its
argument name:
fn hello(req: &Request<Body>, name: &str) -> Result<Response<Body>> {
    allow_method!(req.method(), &Method::GET | &Method::HEAD);
    Ok(ok_with_text(format!("Hello, {}!", name)))
}
	Update route to accept segments as a parameter and match against them:
fn route(req: &Request<Body>, segments: &[&str]) -> Result<Response<Body>> {
    match segments {
        ["hello"] => hello(req, "World"),
        _ => Ok(not_found()),
    }
}
	Update handle to split the path into segments (dropping empty components
between duplicate /):
fn handle(req: Request<Body>) -> Response<Body> {
    // Extract the segments from the URI path.
    let path = req.uri().path().to_owned();
    let segments: Vec<&str> =
        path.split('/').filter(|s| !s.is_empty()).collect();
    // Pass the segments to the routing handler.
    route(&req, &segments).unwrap_or_else(|err| {
        eprintln!("Error: {:#}", err);
        internal_server_error()
    })
}
	It is impossible to distinguish /hello and /hello/ from the segments alone,
but you will see how to handle this situation in § Handlers § Trailing
slashes.
§Normalization
Akin to filesystem paths, URL paths can contain . and .. components, such
that /world/../hello/. is equivalent to /hello. Transforming a path into
its shortest equivalent, by eliminating these components, is called
normalization. Behind a reverse proxy and with well-behaved clients, you may
already receive normalized URLs, otherwise, you can add normalization to
handle:
fn handle(req: Request<Body>) -> Response<Body> {
    /*
     * Extract and normalize the segments from the URI path.
     */
    let path = req.uri().path().to_owned();
    let mut segments = Vec::new();
    for s in path.split('/') {
        match s {
            "" | "." => {}
            ".." => {
                segments.pop();
            }
            s => segments.push(s),
        }
    }
    // Pass the segments to the routing handler.
    route(&req, &segments).unwrap_or_else(|err| {
        eprintln!("Error: {:#}", err);
        internal_server_error()
    })
}
	Test with /world/../hello/.:
$ curl -i http://localhost:3000/world/../hello/.
HTTP/1.1 200 OK
content-type: text/plain; charset=utf-8
content-length: 13
date: Thu, 1 Jan 1970 00:00:00 GMT
Hello, World!%
	§Segment matching
As opposed to static URLs, dynamic URLs contain parameters. For example,
/tasks/3 and /tasks/42 correspond to the same pattern /tasks/<id>, where
the id segment is dynamic. These parameters must be extracted from a routing
handler, and passed to a resource handler as arguments.
§Match a single segment
You can use Rust pattern matching to bind the second segment to the variable
name and pass it as an argument to hello:
fn route(req: &Request<Body>, segments: &[&str]) -> Result<Response<Body>> {
    match segments {
        ["hello"] => hello(req, "World"),
        ["hello", name] => hello(req, name),
        _ => Ok(not_found()),
    }
}
	Try /hello/Jane:
curl -i http://localhost:3000/hello/Jane
HTTP/1.1 200 OK
content-type: text/plain; charset=utf-8
content-length: 12
date: Thu, 1 Jan 1970 00:00:00 GMT
Hello, Jane!%
	§Match multiple segments
You can also bind multiple segments (forming a sub-slice) to a variable with
the .. placeholder (matching zero or more elements). To extract a path, you
can join these segments with /:
fn route(req: &Request<Body>, segments: &[&str]) -> Result<Response<Body>> {
    match segments {
        ["hello"] => hello(req, "World"),
        ["hello", name] => hello(req, name),
        ["hello", s @ ..] => hello(req, &s.join("/")),
        _ => Ok(not_found()),
    }
}
	Try /hello/r/rust:
curl -i http://localhost:3000/hello/r/rust
HTTP/1.1 200 OK
content-type: text/plain; charset=utf-8
content-length: 14
date: Thu, 1 Jan 1970 00:00:00 GMT
Hello, r/rust!%
	§Additional patterns
There are a number of patterns that you can play with:
match segments {
    // Or pattern.
    [] | ["index.html" | "index.htm"] => ...,
    // One trailing segment.
    ["hello1", _] => ...,
    // One or more trailing segment.
    ["hello+", s @ ..] if !s.empty() => ...,
    // Zero or more trailing segments.
    ["hello*", ..] => ...,
    // Last segment.
    ["hello$", .., last] => ...,
    // Custom guard.
    s if is_match(s) => ...,
    _ => Ok(not_found()),
}
	§Nested routing
If you can extract multiple segments from a URL, you can pass them to a sub-routing handler:
fn route_hello(
    req: &Request<Body>,
    segments: &[&str],
) -> Result<Response<Body>> {
    match segments {
        [] => hello(req, "World"),
        [name] => hello(req, name),
        _ => Ok(not_found()),
    }
}
	From route, call route_hello with the remaining segments:
fn route(req: &Request<Body>, segments: &[&str]) -> Result<Response<Body>> {
    match segments {
        ["hello", s @ ..] => route_hello(req, s),
        _ => Ok(not_found()),
    }
}
	§Handlers
The routing handlers extract segments from the target and pass them as a string to the resource handlers. Then, it is the responsibility of the resource handler to parse these parameters into their expected type.
§Trailing slashes
Since both /hello and /hello/ correspond to the same segment hello, a
request to any of these targets is routed to the hello handler:
$ curl -i http://localhost:3000/hello
HTTP/1.1 200 OK
content-type: text/plain; charset=utf-8
content-length: 13
date: Thu, 1 Jan 1970 00:00:00 GMT
Hello, World!%
	$ curl -i http://localhost:3000/hello/
HTTP/1.1 200 OK
content-type: text/plain; charset=utf-8
content-length: 13
date: Thu, 1 Jan 1970 00:00:00 GMT
Hello, World!%
	Although these two URLs point to the same resource, they are not strictly
identical. The page is duplicated, which is undesirable from an SEO
perspective. Additionally, it would be extremely surprising if /hello and
/hello/ pointed to a different resource. Therefore, each handler should
enforce a canonical URL with or without a trailing /.
From an historical point of view, a trailing / indicates a directory, whereas
no trailing / indicates a file. For directories, the server returns the
content of ./index.html. For example, a static website might have the
following layout:
website
├── index.html
├── posts
│   ├── index.html
│   └── my-first-post
│       ├── image.jpg
│       └── index.html
└── static
    └── main.css
	Each folder encapsulates a piece of content. From
/website/posts/my-first-post/, you can link to the image easily with
./image.jpg.
Dynamic web servers can return whatever they want for any URL, but I try to follow these rules:
- For static assets, no trailing slashes.
 - For HTML pages, trailing slashes.
 - For API endpoints, no trailing slashes.
 
Feel free to follow any rules you want, as long as you stay consistent. To
redirect the client, use a 308 Permanent Redirect and a Location header for
the target URL:
fn permanent_redirect<S: AsRef<str>>(url: S) -> Response<Body> {
    Response::builder()
        .status(StatusCode::PERMANENT_REDIRECT)
        .header(header::LOCATION, url.as_ref())
        .body(Body::empty())
        .unwrap()
}
	If you want to enforce no trailing slashes, you can redirect the client when
the path ends with /:
fn hello(req: &Request<Body>, name: &str) -> Result<Response<Body>> {
    allow_method!(req.method(), &Method::GET | &Method::HEAD);
    // Redirect if the path ends with `/`.
    if req.uri().path().ends_with('/') {
        let path = req.uri().path().trim_end_matches('/');
        // Do not redirect the index (always has a trailing `/`).
        if !path.is_empty() {
            // Keep query parameters.
            if let Some(q) = req.uri().query() {
                let mut path = path.to_owned();
                path.push('?');
                path.push_str(q);
                return Ok(permanent_redirect(path));
            }
            return Ok(permanent_redirect(path));
        }
    }
    Ok(ok_with_text(format!("Hello, {}!", name)))
}
	Now, /hello/ redirects permanently to /hello:
$ curl -i --head http://localhost:3000/hello/
HTTP/1.1 308 Permanent Redirect
location: /hello
content-length: 0
date: Thu, 1 Jan 1970 00:00:00 GMT
	You can handle the redirection in a routing handler (or even the reverse proxy) if all the resource handlers use the same convention, but if at some point you want to serve something that resembles a file, like an Atom feed, then it becomes an issue.
Finally, to reduce code duplication, you can use the following macros:
macro_rules! redirect_trailing_slash {
    ($req:expr) => {{
        let path = $req.uri().path();
        if !path.ends_with('/') {
            let mut path = path.to_owned();
            path.push('/');
            if let Some(q) = $req.uri().query() {
                path.push('?');
                path.push_str(q);
            }
            return Ok(self::permanent_redirect(path));
        }
    }};
}
macro_rules! redirect_no_trailing_slash {
    ($req:expr) => {{
        let path = $req.uri().path();
        if path.ends_with('/') {
            let path = path.trim_end_matches('/');
            if !path.is_empty() {
                if let Some(q) = $req.uri().query() {
                    let mut path = path.to_owned();
                    path.push('?');
                    path.push_str(q);
                    return Ok(self::permanent_redirect(path));
                }
                return Ok(self::permanent_redirect(path));
            }
        }
    }};
}
	§Path parameters
The URL segments are string slices, so you have to parse them into their
expected type. For an invalid request, the server returns 400 Bad Request:
fn bad_request<S: Into<String>>(text: S) -> Response<Body> {
    Response::builder()
        .status(StatusCode::BAD_REQUEST)
        .header(header::CONTENT_TYPE, "text/plain; charset=utf-8")
        .body(Body::from(text.into()))
        .unwrap()
}
	In the following example, the parameter is parsed as an usize that indicates
the language id for the response. If the parameter is not a number, the handler
returns 400 Bad Request, if it is out-of-range, it returns 404 Not Found:
fn hello(req: &Request<Body>, id: &str) -> Result<Response<Body>> {
    allow_method!(req.method(), &Method::GET | &Method::HEAD);
    redirect_no_trailing_slash!(req);
    let id = match id.parse::<usize>() {
        Ok(n) => n,
        Err(e) => return Ok(bad_request(e.to_string())),
    };
    let hello = match ["Hello", "Nǐn hǎo", "Namaste", "Hola"].get(id) {
        Some(&s) => s,
        None => return Ok(not_found()),
    };
    Ok(ok_with_text(hello))
}
	Update route to pass the id to hello:
fn route(req: &Request<Body>, segments: &[&str]) -> Result<Response<Body>> {
    match segments {
        ["hello", id] => hello(req, id),
        _ => Ok(not_found()),
    }
}
	With 3, it works as expected:
$ curl -i http://localhost:3000/hello/3
HTTP/1.1 200 OK
content-type: text/plain; charset=utf-8
content-length: 4
date: Thu, 1 Jan 1970 00:00:00 GMT
Hola%
	With 42, it returns 404 Not Found, since the index is out-of-range:
$ curl -i http://localhost:3000/hello/42
HTTP/1.1 404 Not Found
content-length: 0
date: Thu, 1 Jan 1970 00:00:00 GMT
	Finally, if the parameter contains characters other than digits, the server
returns 400 Bad Request with an error message:
$ curl -i http://localhost:3000/hello/world
HTTP/1.1 400 Bad Request
content-type: text/plain; charset=utf-8
content-length: 29
date: Thu, 1 Jan 1970 00:00:00 GMT
invalid digit found in string%
	§Query parameters
Besides the method and the target, a request can contain URL encoded query
parameters after the delimiter ?, such as the parameter lang with the value
en in /hello?lang=en.
These parameters are accessible with req.uri().query(), and you can parse
them as a key/value list with the crate form_urlencoded:
fn hello(req: &Request<Body>) -> Result<Response<Body>> {
    allow_method!(req.method(), &Method::GET | &Method::HEAD);
    redirect_no_trailing_slash!(req);
    let mut lang = "en".to_owned();
    if let Some(query) = req.uri().query() {
        for (k, v) in form_urlencoded::parse(query.as_bytes()) {
            if k == "lang" {
                lang = v.into_owned();
            }
        }
    }
    let hello = match &*lang {
        "en" => "Hello",
        "zh" => "Nǐn hǎo",
        "hi" => "Namaste",
        "es" => "Hola",
        _ => {
            return Ok(bad_request(format!("Unknown language code `{}`", lang)))
        }
    };
    Ok(ok_with_text(hello))
}
	Try /hello?lang=hi:
$ curl -i 'http://localhost:3000/hello?lang=hi'
HTTP/1.1 200 OK
content-type: text/plain; charset=utf-8
content-length: 7
date: Thu, 1 Jan 1970 00:00:00 GMT
Namaste%
	§Form parameters
HTML forms commonly use the same URL encoding to send their data, except when
the method attribute on the form element is set to POST. In this case,
the parameters are submitted through the body of the request.
Reading from the body is an asynchronous operation, so you need to make
hello, route, and handle asynchronous by:
- Replacing 
fnwithasync fn. - Calling them with 
.await. 
Update hello to allow POST and extract the name parameter from the
request body (you need a mutable reference to the request for this operation
consumes the body):
async fn hello(req: &mut Request<Body>) -> Result<Response<Body>> {
    allow_method!(req.method(), &Method::GET | &Method::HEAD | &Method::POST);
    redirect_no_trailing_slash!(req);
    if req.method() == Method::POST {
        let body = hyper::body::to_bytes(req.body_mut()).await?;
        let mut name = None;
        for (k, v) in form_urlencoded::parse(&body) {
            if k == "name" {
                name = Some(v.into_owned());
            }
        }
        let name = match name {
            Some(s) => s,
            None => return Ok(bad_request("Missing `name`")),
        };
        Ok(ok_with_text(format!("Hello, {}!", name)))
    } else {
        Ok(ok_with_text("Hello, World!"))
    }
}
	Update route to pass a &mut Request:
async fn route(
    req: &mut Request<Body>,
    segments: &[&str],
) -> Result<Response<Body>> {
    match segments {
        ["hello"] => hello(req).await,
        _ => Ok(not_found()),
    }
}
	In handle, mark the request as mutable and pass a mutable reference to
route:
async fn handle(mut req: Request<Body>) -> Response<Body> {
    // Extract the segments from the URI path.
    let path = req.uri().path().to_owned();
    let segments: Vec<&str> =
        path.split('/').filter(|s| !s.is_empty()).collect();
    route(&mut req, &segments).await.unwrap_or_else(|err| {
        eprintln!("Error: {:#}", err);
        internal_server_error()
    })
}
	With the method GET:
$ curl -i http://localhost:3000/hello
HTTP/1.1 200 OK
content-type: text/plain; charset=utf-8
content-length: 13
date: Thu, 1 Jan 1970 00:00:00 GMT
Hello, World!%
	With the method POST, pass a name with the -d option:
$ curl -i -X POST -d name=Jane http://localhost:3000/hello
HTTP/1.1 200 OK
content-type: text/plain; charset=utf-8
content-length: 12
date: Thu, 1 Jan 1970 00:00:00 GMT
Hello, Jane!%
	Without a name, you get 400 Bad Request:
$ curl -i -X POST -d foo=bar http://localhost:3000/hello
HTTP/1.1 400 Bad Request
content-type: text/plain; charset=utf-8
content-length: 14
date: Thu, 1 Jan 1970 00:00:00 GMT
Missing `name`%
	§Middlewares
With a router, middlewares are needed to perform operations before or after the handler associated with a route is invoked. With an explicit routing approach, you can perform any operation directly.
§Logging
Update handle to log some information about the connection, the request, and
the response:
fn handle(req: Request<Body>, remote_addr: IpAddr) -> Response<Body> {
    let time = Instant::now();
    let path = req.uri().path().to_owned();
    let segments: Vec<&str> =
        path.split('/').filter(|s| !s.is_empty()).collect();
    let resp = route(&req, &segments).unwrap_or_else(|err| {
        eprintln!("Error: {:#}", err);
        internal_server_error()
    });
    eprintln!(
        "{} {} {} {} {:?}",
        remote_addr,
        req.method(),
        req.uri(),
        resp.status(),
        time.elapsed(),
    );
    resp
}
	You need to edit main to pass the remote IP address to handle (add move
on the inner closure and async blocks):
#[tokio::main]
async fn main() {
    let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
    eprintln!("Listening on {}", addr);
    let make_service = make_service_fn(|conn: &AddrStream| {
        // Get the remote IP address.
        let remote_addr = conn.remote_addr().ip();
        // Move it to this async block.
        async move {
            // Move it to the closure, and move it again to the inner async
            // block.
            Ok::<_, Infallible>(service_fn(move |req| async move {
                Ok::<_, Infallible>(handle(req, remote_addr))
            }))
        }
    });
    if let Err(e) = Server::bind(&addr).serve(make_service).await {
        eprintln!("Error: {:#}", e);
        std::process::exit(1);
    }
}
	After these changes, make some requests and inspect the logs:
$ cargo run --bin 410-logging
Listening on 127.0.0.1:3000
127.0.0.1:36180 GET / 200 OK 127.046µs
127.0.0.1:36182 GET /foo 404 Not Found 22.246µs
127.0.0.1:36184 POST /foo 501 Not Implemented 8.398µs
	§Headers
Behind a reverse proxy, the remote address corresponds to the IP address of the
proxy itself. The real client IP address can be transmitted in the header
X-Forwarded-For. You can extend the request to extract and save this address
in its extension map:
struct ClientAddr(IpAddr);
trait ClientAddrExt {
    fn client_addr(&self) -> Option<IpAddr>;
    fn set_client_addr(&mut self, remote_addr: IpAddr);
}
impl ClientAddrExt for Request<Body> {
    fn client_addr(&self) -> Option<IpAddr> {
        self.extensions().get::<ClientAddr>().map(|a| a.0)
    }
    fn set_client_addr(&mut self, remote_addr: IpAddr) {
        let addr = self
            .headers()
            .get("X-Forwarded-For")
            .and_then(|v| v.to_str().ok())
            .and_then(|s| s.split(',').next())
            .and_then(|s| s.trim().parse::<IpAddr>().ok())
            .unwrap_or(remote_addr);
        self.extensions_mut().insert(ClientAddr(addr));
    }
}
	Update handle to use this extension:
fn handle(mut req: Request<Body>, remote_addr: IpAddr) -> Response<Body> {
    let time = Instant::now();
    let path = req.uri().path().to_owned();
    let segments: Vec<&str> =
        path.split('/').filter(|s| !s.is_empty()).collect();
    req.set_client_addr(remote_addr);
    let resp = route(&req, &segments).unwrap_or_else(|err| {
        eprintln!("Error: {:#}", err);
        internal_server_error()
    });
    eprintln!(
        "{} {} {} {} {:?}",
        req.client_addr().unwrap(),
        req.method(),
        req.uri(),
        resp.status(),
        time.elapsed(),
    );
    resp
}
	Make a regular request:
$ curl http://localhost:3000/ > /dev/null
	Then, make another request with the header X-Forwarded-For, as if the server
was behind a reverse proxy:
$ curl -H 'X-Forwarded-For: 192.0.2.1' http://localhost:3000/ > /dev/null
	You can see both addresses in the log:
$ cargo run --bin 420-client-addr
Listening on 127.0.0.1:3000
127.0.0.1 GET / 200 OK 174.476µs
192.0.2.1 GET / 200 OK 246.638µs
	§Cookies
A server can set cookies for a client with the header Set-Cookie. On each
subsequent request, the client sends back the cookies in the header Cookie.
The crate cookie manages cookies in a CookieJar: you can lookup their
values, add new ones, modify them, and most importantly, get the delta after
you made changes.
Extend Request to extract the original cookies from the headers and return a
CookieJar:
trait CookiesExt {
    fn cookies(&self) -> CookieJar;
}
impl CookiesExt for Request<Body> {
    fn cookies(&self) -> CookieJar {
        let mut jar = CookieJar::new();
        // Iterate on the Cookie header instances.
        for value in self.headers().get_all(header::COOKIE) {
            // Get the name-value pairs separated by semicolons.
            let it = match value.to_str() {
                Ok(s) => s.split(';').map(str::trim),
                Err(_) => continue,
            };
            // Iterate on the pairs.
            for s in it {
                // Parse and add the cookie to the jar.
                if let Ok(c) = Cookie::parse(s.to_owned()) {
                    jar.add_original(c);
                }
            }
        }
        jar
    }
}
	This example uses a cookie to perform authentication. It relies on a session
cookie named session_id with a special value for administrator sessions:
const ADMIN_SESSION_ID: &str = "aec070645fe53ee3b3763059376134f0";
	In practice, you would generate a new id for each session, and store it in a
database with the associated user, privileges, expiration time, etc. If a
client tries to access a restricted page without the appropriate permission,
the server responds with 403 Forbidden:
fn forbidden() -> Response<Body> {
    Response::builder()
        .status(StatusCode::FORBIDDEN)
        .body(Body::empty())
        .unwrap()
}
	The admin handler authenticates the client by comparing the session cookie
value with the admin session id:
async fn admin(req: &mut Request<Body>) -> Result<Response<Body>> {
    allow_method!(req.method(), &Method::GET | &Method::HEAD);
    redirect_no_trailing_slash!(req);
    // Match session_id against ADMIN_SESSION_ID.
    if req.cookies().get("session_id").map(Cookie::value)
        != Some(ADMIN_SESSION_ID)
    {
        return Ok(forbidden());
    }
    Ok(ok_with_text("Authenticated"))
}
	To login, the client must send a valid password to receive the admin session cookie:
async fn login(req: &mut Request<Body>) -> Result<Response<Body>> {
    allow_method!(req.method(), &Method::POST);
    redirect_no_trailing_slash!(req);
    let body = hyper::body::to_bytes(req.body_mut()).await?;
    let mut password = None;
    for (k, v) in form_urlencoded::parse(&body) {
        if k == "password" {
            password = Some(v.into_owned());
        }
    }
    let password = match password {
        Some(s) => s,
        None => return Ok(bad_request("Missing `password`")),
    };
    if password != "hunter2" {
        return Ok(forbidden());
    }
    // Get the cookie jar.
    let mut jar = req.cookies();
    // Build a session cookie.
    let cookie = Cookie::build("session_id", ADMIN_SESSION_ID)
        .path("/")
        .secure(false) // Do not require HTTPS.
        .http_only(true)
        .same_site(cookie::SameSite::Lax)
        .finish();
    // Add it to the jar.
    jar.add(cookie);
    // Prepare the response to redirect to /admin.
    let mut resp = Response::builder()
        .status(StatusCode::SEE_OTHER)
        .header(header::LOCATION, "/admin");
    // Set the changed cookies.
    for cookie in jar.delta() {
        resp = resp.header(header::SET_COOKIE, cookie.to_string());
    }
    // Return with an empty body.
    Ok(resp.body(Body::empty()).unwrap())
}
	Update route to forward requests to admin and login:
async fn route(
    req: &mut Request<Body>,
    segments: &[&str],
) -> Result<Response<Body>> {
    match segments {
        ["admin"] => admin(req).await,
        ["login"] => login(req).await,
        _ => Ok(not_found()),
    }
}
	Without a valid session id, the admin page returns 403 Forbidden:
curl -i http://localhost:3000/admin
HTTP/1.1 403 Forbidden
content-length: 0
date: Thu, 1 Jan 1970 00:00:00 GMT
	To get the admin cookie, POST the password to the login endpoint (a web
browser would then save this cookie and follow the redirection):
curl -i -X POST -d password=hunter2 http://localhost:3000/login
HTTP/1.1 303 See Other
location: /admin
set-cookie: session_id=aec070645fe53ee3b3763059376134f0; HttpOnly; SameSite=Lax; Path=/
content-length: 0
date: Thu, 1 Jan 1970 00:00:00 GMT
	Finally, request the admin page with this cookie:
curl -i --cookie 'session_id=aec070645fe53ee3b3763059376134f0' http://localhost:3000/admin
HTTP/1.1 200 OK
content-type: text/plain; charset=utf-8
content-length: 13
date: Thu, 1 Jan 1970 00:00:00 GMT
Authenticated%
	§Context
In a real web application, the handlers would need to access external or shared resources: the application settings, a database, a templating engine, a job queue. These resources can be shared with the handlers through a context passed along with the request.
To share the same data between multiple threads, you need to use an Arc<T>
pointer, where T is your Context. It contains a boolean setting to enable
debug mode and a Vec<String> that emulates a table in a database with a
single text column. To allow mutable access, place the Vec inside a
tokio::sync::Mutex:
struct Context {
    debug: bool,
    data: Mutex<Vec<String>>,
}
	For the method GET, the list handler returns the list of rows; for the
method POST, it adds a new row based on the form parameter text:
async fn list(
    ctx: &Context,
    req: &mut Request<Body>,
) -> Result<Response<Body>> {
    allow_method!(req.method(), &Method::GET | &Method::HEAD | &Method::POST);
    if req.method() == Method::POST {
        let body = hyper::body::to_bytes(req.body_mut()).await?;
        let mut text = None;
        for (k, v) in form_urlencoded::parse(&body) {
            if k == "text" {
                text = Some(v.into_owned());
            }
        }
        let text = match text {
            Some(s) => s,
            None => return Ok(bad_request("Missing `text`")),
        };
        let mut v = ctx.data.lock().await;
        v.push(text);
        return Ok(created(format!("/{}", v.len() - 1)));
    }
    let mut output = String::new();
    for (id, text) in ctx.data.lock().await.iter().enumerate() {
        writeln!(&mut output, "{} | {}", id, text).unwrap();
    }
    Ok(ok_with_text(output))
}
	The details handler returns the text from the given row:
async fn details(
    ctx: &Context,
    req: &Request<Body>,
    id: &str,
) -> Result<Response<Body>> {
    allow_method!(req.method(), &Method::GET | &Method::HEAD);
    let id = match id.parse::<usize>() {
        Ok(n) => n,
        Err(e) => return Ok(bad_request(e.to_string())),
    };
    match ctx.data.lock().await.get(id) {
        Some(s) => Ok(ok_with_text(s)),
        None => Ok(not_found()),
    }
}
	Update route to call these two resource handlers and pass a mutable request:
async fn route(
    ctx: &Context,
    req: &mut Request<Body>,
    segments: &[&str],
) -> Result<Response<Body>> {
    if ctx.debug {
        eprintln!("{:?}", req);
    }
    match segments {
        [] => list(ctx, req).await,
        [id] => details(ctx, req, id).await,
        _ => Ok(not_found()),
    }
}
	Update handle to pass a mutable request to route:
async fn handle(ctx: Arc<Context>, mut req: Request<Body>) -> Response<Body> {
    let path = req.uri().path().to_owned();
    let segments: Vec<&str> =
        path.split('/').filter(|s| !s.is_empty()).collect();
    route(&ctx, &mut req, &segments)
        .await
        .unwrap_or_else(|err| {
            eprintln!("Error: {:#}", err);
            internal_server_error()
        })
}
	In main, the context is initialized, put inside an Arc, then it is cloned
and shared with the handlers:
#[tokio::main]
async fn main() {
    let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
    eprintln!("Listening on {}", addr);
    // Initialize the context.
    let ctx = Arc::new(Context {
        debug: true,
        data: Mutex::new(Vec::new()),
    });
    let make_service = make_service_fn(|_conn| {
        // Clone the pointer for each connection.
        let ctx = Arc::clone(&ctx);
        async move {
            Ok::<_, Infallible>(service_fn(move |req| {
                // Clone the pointer for each request.
                let ctx = Arc::clone(&ctx);
                async move { Ok::<_, Infallible>(handle(ctx, req).await) }
            }))
        }
    });
    if let Err(e) = Server::bind(&addr).serve(make_service).await {
        eprintln!("Error: {:#}", e);
        std::process::exit(1);
    }
}
	Initially, there is no data:
$ curl -i http://localhost:3000/
HTTP/1.1 200 OK
content-type: text/plain; charset=utf-8
content-length: 0
date: Thu, 1 Jan 1970 00:00:00 GMT
	With the method POST, you can add new rows:
$ curl -i -X POST -d text=hello http://localhost:3000/
HTTP/1.1 201 Created
location: /0
content-length: 0
date: Thu, 1 Jan 1970 00:00:00 GMT
	$ curl -i -X POST -d text=hola http://localhost:3000/
HTTP/1.1 201 Created
location: /1
content-length: 0
date: Thu, 1 Jan 1970 00:00:00 GMT
	$ curl -i -X POST -d text=aloha http://localhost:3000/
HTTP/1.1 201 Created
location: /2
content-length: 0
date: Thu, 1 Jan 1970 00:00:00 GMT
	The new rows appear in the list:
$ curl -i http://localhost:3000/
HTTP/1.1 200 OK
content-type: text/plain; charset=utf-8
content-length: 11
date: Thu, 1 Jan 1970 00:00:00 GMT
0 | hello
1 | hola
2 | aloha
	You can view a single one given its id:
$ curl -i http://localhost:3000/0
HTTP/1.1 200 OK
content-type: text/plain; charset=utf-8
content-length: 5
date: Thu, 1 Jan 1970 00:00:00 GMT
hello%
	§Conclusion
If you decide to build a web server from scratch, I think Rust and Hyper are solid options against micro-frameworks such as Flask. Hyper provides the building blocks for asynchronous, safe, "low-level" HTTP handling, and Rust pattern matching is powerful enough to replace a router. Starting from here, you can add anything to the resource handlers. For example, this website relies on the same techniques, with the addition of content and asset management, an SQLite database, HTML templating, etc.