Http Server In Rust - Functional Way

July 8, 2025

My primary development expertise lies in the JVM ecosystem. I started with Java and then, fortunately, transitioned to Scala and functional programming (FP). Since then, I’ve been infected with FP :) Unfortunately, despite Scala’s strengths, its popularity has significantly declined in recent years. In Australia, for example, Scala job opportunities are virtually nonexistent. Java, while still widely used, is no longer as hot as it once was—thankfully, there’s TypeScript. Personally, I find coding in Java far less exciting than working with languages that offer better support for functional programming.

Without heavy use of features like algebraic data types (ADTs), newtypes, pattern matching, and functional composition—where you rely heavily on the compiler—it becomes difficult to write maintainable and less buggy code.

Last year, I began exploring Rust and subscribed to https://codecrafters.io/ to deepen my learning. While the subscription is somewhat pricey, the platform offers valuable hands-on challenges. I started with the codecrafters-kafka-rust challenge, which provided an excellent introduction to Rust’s syntax and core concepts. More recently, I completed the codecrafters-http-server-rust challenge. After passing all of CodeCrafters’ tests, I decided to refactor the code into a more functional style than it originally had.

“Build Your Own HTTP server” Challenge.

http-server-tasks

First, I’d like to present the results and then provide some context.


#[tokio::main]
async fn main() -> Result<()> {
    let server = Server::bind("127.0.0.1:4221").await?;
    server
        .serve(Arc::new(async move |state| {
            routes().handle(state).map(|v| v.0)
        }))
        .await
}

pub fn routes() -> impl Endpoint<Output=UnitT> {
    let v = user_agent()
        .or(route::get("/echo").set_response(path().flat_map(|v| ok(v))))
        .or(get_file())
        .or(post_file())
        .or(route::get("/").set_response(ok("")))
        .or(state().set_response(not_found("")))
        .and(gzip().and(close_connection()))
        .unit();

    v
}
fn user_agent() -> impl Endpoint<Output=UnitT> {
    let response = |v: Option<UserAgent>| ok(v.map(|v| v.0).unwrap_or("".to_string()));

    route::get("/user-agent").set_response(get_user_agent().flat_map(response))
}

fn get_file() -> impl Endpoint<Output=UnitT> {
    let read = |file: String| {
        lift(
            match format!("/tmp/data/codecrafters.io/http-server-tester/{}", file)
                .as_str()
                .read()
            {
                Ok(data) => mk_response(data, StatusCode::SC200),
                Err(_) => mk_response("", StatusCode::SC404),
            },
        )
    };
    route::get("/files").set_response(path().flat_map(read))
}
fn post_file() -> impl Endpoint<Output=UnitT> {
    let response = |(file, body): (String, Option<RequestBody>)| {
        lift(
            match format!("/tmp/data/codecrafters.io/http-server-tester/{}", file)
                .as_str()
                .write(body.unwrap().0)
            {
                Ok(_) => mk_response("", StatusCode::SC201),
                Err(_) => mk_response("", StatusCode::SC404),
            },
        )
    };
    route::post("/files").set_response(path().and(req_body()).flat_map(response))
}

To parse HTTP request byte streams, I use an excellent library called Nom , a parser combinator for Rust. It provides a great example of how combinators can be built in Rust. Inspired by this approach, I created my own set of HTTP combinators.

Implimentation

There is a trait Endpoint that has one abstract function. fn handle(&self, r: State) -> Result<(State, Self::Output)>; All combinators have to implement it;

I will explain the main idea on the Map combinator

pub trait Endpoint {
    type Output: Debug + Clone;

    fn handle(&self, r: State) -> Result<(State, Self::Output)>;

    fn map<F, O2>(self, f: F) -> Map<Self, F>
    where
        F: Fn(Self::Output) -> O2,
        Self: Sized,
    {
        Map { g: self, f }
    }
}

// Implementation

pub struct Map<G, F> {
    g: G,
    f: F,
}

impl<G, F, O2> Endpoint for Map<G, F>
where
    O2: Debug + Clone,
    G: Endpoint,
    F: Fn(G::Output) -> O2,
{
    type Output = O2;
    fn handle(&self, r: State) -> Result<(State, Self::Output)> {
        let (state, o) = self.g.handle(r)?;
        Ok((state, (self.f)(o)))
    }
}
//This an example how map is used in a path combinator which returns path of the request

pub fn path() -> impl Endpoint<Output=String> {
    request().map(|v| v.get_path())
}

A few words about handle. It functions like a State monad: a function that takes a State and returns a State along with the function’s result. The State type has two variants: Incomplete, which contains only an HTTP Request, and Complete, which includes both a Request and a Response.

#[derive(Debug, Clone)]
pub struct Incomplete(RequestRef);
#[derive(Debug, Clone)]
pub struct Complete(pub RequestRef, pub ResponseRef);
#[derive(Debug, Clone)]
pub enum State {
    Incomplete(Incomplete),
    Complete(Complete),
}

RequestRef acts as a Reader, providing read-only access to request data for use in combinators.

ResponseRef acts as a State, allowing combinators to modify it.

This provides a high-level overview.

The implementation is limited to the requirements of the challenge.

Thank you !