Building a Minimalist Rust HTTP Server without Frameworks
Ethan Miller
Product Engineer · Leapcell

Introduction
In the vibrant Rust ecosystem, numerous web frameworks like Axum, Actix-web, and Warp offer a streamlined path to building robust HTTP services. These frameworks provide convenient abstractions, battle-tested middleware, and extensive feature sets, significantly accelerating development. However, for those keen on understanding the foundational mechanics of an HTTP server, or for projects demanding extreme control and minimal dependencies, bypassing these frameworks and building directly on core libraries becomes invaluable. This exploration unpacks the process of constructing a minimalist Rust HTTP server leveraging hyper for the HTTP protocol and tokio for asynchronous runtime and I/O. Such an endeavor not only deepens one's comprehension of asynchronous programming in Rust but also showcases the power and flexibility of its core libraries, leading to efficient and highly customizable solutions.
Core Components for a Raw HTTP Server
Before we dive into the code, let's briefly define the central components we'll be using:
tokio: This is a powerful asynchronous runtime for Rust. It provides the necessary tools for non-blocking I/O, task scheduling, and managing futures. In our server,tokiowill handle listening for incoming connections and efficiently managing concurrent requests.hyper: A fast and correct HTTP implementation written in Rust.hyperoperates at a lower level than web frameworks, dealing directly with HTTP/1.1 and HTTP/2 requests and responses. It handles the parsing of incoming HTTP requests and the formatting of outgoing HTTP responses, abstracting away the complexities of the HTTP protocol itself but not the application's request handling logic.async/await: Rust's built-in syntax for writing asynchronous code. It allows us to write non-blocking code that looks and feels like synchronous code, making asynchronous programming more ergonomic.
Understanding how these pieces fit together is crucial. tokio provides the asynchronous execution environment and the means to accept TCP connections. Once a connection is established, hyper takes over to parse the incoming byte stream into an HTTP request and then serialize an HTTP response back into a byte stream to be sent over the network. Our application logic will reside between these two steps, processing the parsed request and generating an appropriate response.
Building the Minimalist Server
Let's begin by setting up our Cargo.toml with the necessary dependencies:
[package] name = "minimal-http-server" version = "0.1.0" edition = "2021" [dependencies] tokio = { version = "1", features = ["full"] } # "full" includes runtime, net, macros, etc. hyper = { version = "0.14", features = ["full"] }
Next, let's write the Rust code for our server. Our goal is to create a server that listens on 127.0.0.1:3000 and responds to all requests with a simple "Hello, world!" message.
use std::{convert::Infallible, net::SocketAddr}; use hyper::{Body, Request, Response, Server}; use hyper::service::{make_service_fn, service_fn}; // The core request handler function async fn handle_request(_req: Request<Body>) -> Result<Response<Body>, Infallible> { // We ignore the incoming request for this simple example Ok(Response::new("Hello, world!".into())) } #[tokio::main] async fn main() { // Define the address to listen on let addr = SocketAddr::from(([127, 0, 0, 1], 3000)); // A `Service` is a trait that represents an asynchronous function from a Request to a Response. // `make_service_fn` is used to create a new `Service` for each incoming connection. let make_svc = make_service_fn(|_conn| async { // `service_fn` is a helper to convert a `Fn` into a `Service` Ok::<_, Infallible>(service_fn(handle_request)) }); // Create the server with the specified address and service factory let server = Server::bind(&addr).serve(make_svc); println!("Listening on http://{}", addr); // Run the server until an error occurs if let Err(e) = server.await { eprintln!("server error: {}", e); } }
Let's break down this code:
usestatements: We import necessary types fromstd,hyper, andhyper::service.Infallibleis used forResulttypes where theErrvariant can never occur, simplifying error handling for our basic example.handle_requestfunction: Thisasyncfunction is our server's core logic. It takes aRequest<Body>as input and returns aResult<Response<Body>, Infallible>. For this example, it ignores the incoming request and always returns anHTTP 200 OKresponse with "Hello, world!" in the body.#[tokio::main]attribute: This macro fromtokiosimplifies setting up the Tokio runtime. It transforms ourmainfunction into an asynchronous entry point and handles initializing the runtime.SocketAddr::from(([127, 0, 0, 1], 3000)): This defines the IP address and port our server will listen on.make_service_fn: This function is crucial forhyperservers. It takes a closure that returns aServiceinstance for each new TCP connection. This pattern allows connection-specific state if needed, though not in our simple case. Its closure needs to beasyncas well.service_fn(handle_request): Insidemake_service_fn,service_fnis used to adapt ourhandle_requestasyncfunction intohyper'sServicetrait implementation, which is what the server expects.Server::bind(&addr).serve(make_svc): This line creates and configures thehyperserver.bindspecifies the address to listen on, andservetakes ourmake_svcfactory to handle incoming connections.server.await: This starts the server. Theawaitkeyword pauses the execution ofmainuntil the server gracefully shuts down or encounters an error.
Running and Testing
To run this server, navigate to your project directory in the terminal and execute:
cargo run
You should see "Listening on http://127.0.0.1:3000". Now, open your web browser or use a tool like curl to access the server:
curl http://127.0.0.1:3000
You should receive the response: Hello, world!
This simple example demonstrates the fundamental building blocks. You can extend handle_request to:
- Parse request paths: 
req.uri().path() - Read request headers: 
req.headers() - Read request body: 
hyper::body::to_bytes(req.into_body()).await - Generate dynamic responses: Construct 
Response<Body>with different statuses, headers, and body content. 
Conclusion
By directly utilizing hyper and tokio, we've successfully crafted a minimalist HTTP server in Rust without relying on higher-level frameworks. This hands-on approach provides profound insights into asynchronous programming, the HTTP protocol, and the architectural choices involved in building performant network services. It underscores Rust's capability to deliver high-performance, low-level network applications while maintaining safety and concurrency, empowering developers to build custom and efficient server solutions from the ground up.