How do I build HTTP servers with hyper in Rust?

Walkthrough

The hyper crate is a fast, correct, and low-level HTTP library for Rust. It's the foundation for popular web frameworks like axum, warp, and actix-web. Hyper provides both client and server functionality with full HTTP/1 and HTTP/2 support. It's built on Tokio for async I/O and offers fine-grained control over HTTP operations. While more verbose than higher-level frameworks, hyper gives you maximum flexibility and performance for building HTTP services.

Key concepts:

  1. Server — binds to an address and handles incoming connections
  2. Service trait — defines how to process requests into responses
  3. Body — streams of bytes for request/response bodies
  4. Request/Response — HTTP message types
  5. Routing — pattern matching on request method and path

Code Example

# Cargo.toml
[dependencies]
hyper = { version = "1.4", features = ["full"] }
tokio = { version = "1", features = ["full"] }
http-body-util = "0.1"
hyper-util = { version = "0.1", features = ["full"] }
use hyper::server::conn::http1;
use hyper::service::service_fn;
use hyper::{Request, Response, Body, Method, StatusCode};
use http_body_util::Full;
use hyper_util::rt::TokioIo;
use tokio::net::TcpListener;
 
async fn hello(req: Request<hyper::body::Incoming>) -> Result<Response<Full<hyper::body::Bytes>>, hyper::Error> {
    Ok(Response::new(Full::new(hyper::body::Bytes::from("Hello, World!"))))
}
 
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
    let addr = "127.0.0.1:3000";
    let listener = TcpListener::bind(addr).await?;
    
    println!("Listening on {}", addr);
    
    loop {
        let (stream, _) = listener.accept().await?;
        let io = TokioIo::new(stream);
        
        tokio::spawn(async move {
            if let Err(err) = http1::Builder::new().serve_connection(io, service_fn(hello)).await {
                eprintln!("Error serving connection: {:?}", err);
            }
        });
    }
}

Basic Server with Routing

use hyper::server::conn::http1;
use hyper::service::service_fn;
use hyper::{Method, Request, Response, StatusCode};
use http_body_util::Full;
use hyper_util::rt::TokioIo;
use tokio::net::TcpListener;
use std::convert::Infallible;
 
async fn handle_request(req: Request<hyper::body::Incoming>) -> Result<Response<Full<hyper::body::Bytes>>, Infallible> {
    match (req.method(), req.uri().path()) {
        (&Method::GET, "/") => {
            Ok(Response::new(Full::new(hyper::body::Bytes::from("Welcome home!"))))
        }
        (&Method::GET, "/health") => {
            Ok(Response::new(Full::new(hyper::body::Bytes::from("OK"))))
        }
        (&Method::POST, "/echo") => {
            Ok(Response::new(Full::new(hyper::body::Bytes::from("Echo endpoint"))))
        }
        _ => {
            let mut not_found = Response::new(Full::new(hyper::body::Bytes::from("Not Found")));
            *not_found.status_mut() = StatusCode::NOT_FOUND;
            Ok(not_found)
        }
    }
}
 
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
    let addr = "127.0.0.1:3000";
    let listener = TcpListener::bind(addr).await?;
    
    println!("Server running at http://{}", addr);
    
    loop {
        let (stream, _) = listener.accept().await?;
        let io = TokioIo::new(stream);
        
        tokio::spawn(async move {
            if let Err(err) = http1::Builder::new()
                .serve_connection(io, service_fn(handle_request))
                .await
            {
                eprintln!("Error: {:?}", err);
            }
        });
    }
}

Reading Request Body

use hyper::body::Bytes;
use hyper::server::conn::http1;
use hyper::service::service_fn;
use hyper::{Method, Request, Response, StatusCode};
use http_body_util::{BodyExt, Full};
use hyper_util::rt::TokioIo;
use tokio::net::TcpListener;
use std::convert::Infallible;
 
async fn handle_request(req: Request<hyper::body::Incoming>) -> Result<Response<Full<Bytes>>, Infallible> {
    match (req.method(), req.uri().path()) {
        (&Method::POST, "/echo") => {
            // Collect the entire body
            let body_bytes = req.collect().await.unwrap().to_bytes();
            
            // Echo it back
            Ok(Response::new(Full::new(body_bytes)))
        }
        (&Method::POST, "/length") => {
            let body_bytes = req.collect().await.unwrap().to_bytes();
            let response = format!("Body length: {} bytes", body_bytes.len());
            
            Ok(Response::new(Full::new(Bytes::from(response))))
        }
        _ => {
            let mut response = Response::new(Full::new(Bytes::from("Not Found")));
            *response.status_mut() = StatusCode::NOT_FOUND;
            Ok(response)
        }
    }
}
 
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
    let listener = TcpListener::bind("127.0.0.1:3000").await?;
    
    loop {
        let (stream, _) = listener.accept().await?;
        let io = TokioIo::new(stream);
        
        tokio::spawn(async move {
            let _ = http1::Builder::new()
                .serve_connection(io, service_fn(handle_request))
                .await;
        });
    }
}

JSON Request and Response

use hyper::body::Bytes;
use hyper::server::conn::http1;
use hyper::service::service_fn;
use hyper::{Method, Request, Response, StatusCode, header};
use http_body_util::{BodyExt, Full};
use hyper_util::rt::TokioIo;
use tokio::net::TcpListener;
use serde::{Deserialize, Serialize};
use std::convert::Infallible;
 
#[derive(Deserialize)]
struct UserRequest {
    name: String,
    email: String,
}
 
#[derive(Serialize)]
struct UserResponse {
    id: u64,
    name: String,
    email: String,
    message: String,
}
 
async fn handle_request(req: Request<hyper::body::Incoming>) -> Result<Response<Full<Bytes>>, Infallible> {
    match (req.method(), req.uri().path()) {
        (&Method::POST, "/users") => {
            // Read body
            let body_bytes = req.collect().await.unwrap().to_bytes();
            
            // Parse JSON
            match serde_json::from_slice::<UserRequest>(&body_bytes) {
                Ok(user) => {
                    // Create response
                    let response = UserResponse {
                        id: 1,
                        name: user.name,
                        email: user.email,
                        message: "User created successfully".to_string(),
                    };
                    
                    let json = serde_json::to_string(&response).unwrap();
                    
                    let mut res = Response::new(Full::new(Bytes::from(json)));
                    res.headers_mut().insert(
                        header::CONTENT_TYPE,
                        header::HeaderValue::from_static("application/json"),
                    );
                    *res.status_mut() = StatusCode::CREATED;
                    Ok(res)
                }
                Err(e) => {
                    let mut res = Response::new(Full::new(Bytes::from(format!("Invalid JSON: {}", e))));
                    *res.status_mut() = StatusCode::BAD_REQUEST;
                    Ok(res)
                }
            }
        }
        _ => {
            let mut res = Response::new(Full::new(Bytes::from("Not Found")));
            *res.status_mut() = StatusCode::NOT_FOUND;
            Ok(res)
        }
    }
}
 
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
    let listener = TcpListener::bind("127.0.0.1:3000").await?;
    println!("Server running at http://127.0.0.1:3000");
    
    loop {
        let (stream, _) = listener.accept().await?;
        let io = TokioIo::new(stream);
        
        tokio::spawn(async move {
            let _ = http1::Builder::new()
                .serve_connection(io, service_fn(handle_request))
                .await;
        });
    }
}

Query Parameters

use hyper::body::Bytes;
use hyper::server::conn::http1;
use hyper::service::service_fn;
use hyper::{Method, Request, Response, Uri};
use http_body_util::Full;
use hyper_util::rt::TokioIo;
use tokio::net::TcpListener;
use std::collections::HashMap;
use std::convert::Infallible;
 
fn parse_query(uri: &Uri) -> HashMap<String, String> {
    let mut params = HashMap::new();
    
    if let Some(query) = uri.query() {
        for pair in query.split('&') {
            if let Some((key, value)) = pair.split_once('=') {
                params.insert(
                    urlencoding::decode(key).unwrap().to_string(),
                    urlencoding::decode(value).unwrap().to_string(),
                );
            }
        }
    }
    
    params
}
 
async fn handle_request(req: Request<hyper::body::Incoming>) -> Result<Response<Full<Bytes>>, Infallible> {
    let params = parse_query(req.uri());
    
    let name = params.get("name").map(|s| s.as_str()).unwrap_or("World");
    let count = params.get("count").and_then(|s| s.parse::<u32>().ok()).unwrap_or(1);
    
    let message = format!("Hello, {}! (x{})", name, count);
    
    Ok(Response::new(Full::new(Bytes::from(message))))
}
 
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
    let listener = TcpListener::bind("127.0.0.1:3000").await?;
    println!("Try: http://127.0.0.1:3000?name=Rust&count=5");
    
    loop {
        let (stream, _) = listener.accept().await?;
        let io = TokioIo::new(stream);
        
        tokio::spawn(async move {
            let _ = http1::Builder::new()
                .serve_connection(io, service_fn(handle_request))
                .await;
        });
    }
}

Path Parameters

use hyper::body::Bytes;
use hyper::server::conn::http1;
use hyper::service::service_fn;
use hyper::{Method, Request, Response, StatusCode};
use http_body_util::Full;
use hyper_util::rt::TokioIo;
use tokio::net::TcpListener;
use std::convert::Infallible;
 
// Simple path segment extraction
fn extract_path_segments(path: &str) -> Vec<&str> {
    path.split('/').filter(|s| !s.is_empty()).collect()
}
 
async fn handle_request(req: Request<hyper::body::Incoming>) -> Result<Response<Full<Bytes>>, Infallible> {
    let path = req.uri().path();
    let segments = extract_path_segments(path);
    
    match (req.method(), segments.as_slice()) {
        (&Method::GET, ["users", id]) => {
            let response = format!("User ID: {}", id);
            Ok(Response::new(Full::new(Bytes::from(response))))
        }
        (&Method::GET, ["users", user_id, "posts", post_id]) => {
            let response = format!("User {}'s Post {}", user_id, post_id);
            Ok(Response::new(Full::new(Bytes::from(response))))
        }
        (&Method::GET, ["users"]) => {
            Ok(Response::new(Full::new(Bytes::from("List all users"))))
        }
        _ => {
            let mut res = Response::new(Full::new(Bytes::from("Not Found")));
            *res.status_mut() = StatusCode::NOT_FOUND;
            Ok(res)
        }
    }
}
 
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
    let listener = TcpListener::bind("127.0.0.1:3000").await?;
    println!("Try:");
    println!("  http://127.0.0.1:3000/users");
    println!("  http://127.0.0.1:3000/users/42");
    println!("  http://127.0.0.1:3000/users/42/posts/1");
    
    loop {
        let (stream, _) = listener.accept().await?;
        let io = TokioIo::new(stream);
        
        tokio::spawn(async move {
            let _ = http1::Builder::new()
                .serve_connection(io, service_fn(handle_request))
                .await;
        });
    }
}

Headers

use hyper::body::Bytes;
use hyper::server::conn::http1;
use hyper::service::service_fn;
use hyper::{header, Method, Request, Response, StatusCode};
use http_body_util::Full;
use hyper_util::rt::TokioIo;
use tokio::net::TcpListener;
use std::convert::Infallible;
 
async fn handle_request(req: Request<hyper::body::Incoming>) -> Result<Response<Full<Bytes>>, Infallible> {
    // Read request headers
    let user_agent = req.headers()
        .get(header::USER_AGENT)
        .and_then(|v| v.to_str().ok())
        .unwrap_or("Unknown");
    
    let content_type = req.headers()
        .get(header::CONTENT_TYPE)
        .and_then(|v| v.to_str().ok())
        .unwrap_or("Not specified");
    
    let auth = req.headers()
        .get("Authorization")
        .and_then(|v| v.to_str().ok())
        .unwrap_or("No auth");
    
    // Build response with custom headers
    let mut res = Response::new(Full::new(Bytes::from(format!(
        "User-Agent: {}\nContent-Type: {}\nAuth: {}",
        user_agent, content_type, auth
    ))));
    
    // Set response headers
    res.headers_mut().insert(
        header::CONTENT_TYPE,
        header::HeaderValue::from_static("text/plain"),
    );
    res.headers_mut().insert(
        header::CACHE_CONTROL,
        header::HeaderValue::from_static("no-cache"),
    );
    res.headers_mut().insert(
        "X-Custom-Header",
        header::HeaderValue::from_static("Custom Value"),
    );
    
    Ok(res)
}
 
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
    let listener = TcpListener::bind("127.0.0.1:3000").await?;
    
    loop {
        let (stream, _) = listener.accept().await?;
        let io = TokioIo::new(stream);
        
        tokio::spawn(async move {
            let _ = http1::Builder::new()
                .serve_connection(io, service_fn(handle_request))
                .await;
        });
    }
}

Static File Server

use hyper::body::Bytes;
use hyper::server::conn::http1;
use hyper::service::service_fn;
use hyper::{header, Method, Request, Response, StatusCode, Body};
use http_body_util::{BodyExt, Full, StreamBody};
use hyper_util::rt::TokioIo;
use tokio::net::TcpListener;
use tokio::fs::File;
use tokio::io::AsyncReadExt;
use std::path::PathBuf;
use std::convert::Infallible;
 
fn content_type(path: &str) -> &'static str {
    match PathBuf::from(path).extension().and_then(|s| s.to_str()) {
        Some("html") => "text/html",
        Some("css") => "text/css",
        Some("js") => "application/javascript",
        Some("json") => "application/json",
        Some("png") => "image/png",
        Some("jpg") | Some("jpeg") => "image/jpeg",
        Some("txt") => "text/plain",
        _ => "application/octet-stream",
    }
}
 
async fn serve_file(path: &str) -> Result<Response<Full<Bytes>>, Infallible> {
    // Remove leading slash and prevent directory traversal
    let safe_path = path.trim_start_matches('/');
    
    if safe_path.contains("..") {
        return Ok(Response::builder()
            .status(StatusCode::FORBIDDEN)
            .body(Full::new(Bytes::from("Forbidden")))
            .unwrap());
    }
    
    let full_path = PathBuf::from("static").join(safe_path);
    
    match File::open(&full_path).await {
        Ok(mut file) => {
            let mut contents = Vec::new();
            if file.read_to_end(&mut contents).await.is_err() {
                return Ok(Response::builder()
                    .status(StatusCode::INTERNAL_SERVER_ERROR)
                    .body(Full::new(Bytes::from("Read error")))
                    .unwrap());
            }
            
            let ct = content_type(path);
            
            Ok(Response::builder()
                .status(StatusCode::OK)
                .header(header::CONTENT_TYPE, ct)
                .body(Full::new(Bytes::from(contents)))
                .unwrap())
        }
        Err(_) => {
            Ok(Response::builder()
                .status(StatusCode::NOT_FOUND)
                .body(Full::new(Bytes::from("Not Found")))
                .unwrap())
        }
    }
}
 
async fn handle_request(req: Request<hyper::body::Incoming>) -> Result<Response<Full<Bytes>>, Infallible> {
    let path = req.uri().path();
    
    // Default to index.html
    let file_path = if path == "/" { "/index.html" } else { path };
    
    serve_file(file_path).await
}
 
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
    let listener = TcpListener::bind("127.0.0.1:3000").await?;
    println!("Static file server running at http://127.0.0.1:3000");
    
    loop {
        let (stream, _) = listener.accept().await?;
        let io = TokioIo::new(stream);
        
        tokio::spawn(async move {
            let _ = http1::Builder::new()
                .serve_connection(io, service_fn(handle_request))
                .await;
        });
    }
}

Streaming Response

use hyper::body::{Bytes, Frame};
use hyper::server::conn::http1;
use hyper::service::service_fn;
use hyper::{Request, Response, header};
use http_body_util::StreamBody;
use hyper_util::rt::TokioIo;
use tokio::net::TcpListener;
use tokio_stream::wrappers::ReceiverStream;
use std::convert::Infallible;
use tokio::sync::mpsc;
use futures_util::StreamExt;
 
async fn handle_request(_req: Request<hyper::body::Incoming>) -> Result<Response<StreamBody<ReceiverStream<Result<Frame<Bytes>, Infallible>>>>, Infallible> {
    let (tx, rx) = mpsc::channel::<Result<Frame<Bytes>, Infallible>>(10);
    
    // Spawn task that sends chunks
    tokio::spawn(async move {
        for i in 0..5 {
            let chunk = format!("Chunk {}\n", i);
            tx.send(Ok(Frame::data(Bytes::from(chunk)))).await.unwrap();
            tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
        }
    });
    
    let stream = ReceiverStream::new(rx);
    let body = StreamBody::new(stream);
    
    let mut response = Response::new(body);
    response.headers_mut().insert(
        header::CONTENT_TYPE,
        header::HeaderValue::from_static("text/plain"),
    );
    
    Ok(response)
}
 
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
    let listener = TcpListener::bind("127.0.0.1:3000").await?;
    println!("Streaming server at http://127.0.0.1:3000");
    
    loop {
        let (stream, _) = listener.accept().await?;
        let io = TokioIo::new(stream);
        
        tokio::spawn(async move {
            let _ = http1::Builder::new()
                .serve_connection(io, service_fn(handle_request))
                .await;
        });
    }
}

Server-Sent Events (SSE)

use hyper::body::{Bytes, Frame};
use hyper::server::conn::http1;
use hyper::service::service_fn;
use hyper::{Request, Response, header};
use http_body_util::StreamBody;
use hyper_util::rt::TokioIo;
use tokio::net::TcpListener;
use tokio_stream::wrappers::ReceiverStream;
use std::convert::Infallible;
use tokio::sync::mpsc;
 
async fn handle_sse(_req: Request<hyper::body::Incoming>) -> Result<Response<StreamBody<ReceiverStream<Result<Frame<Bytes>, Infallible>>>>, Infallible> {
    let (tx, rx) = mpsc::channel(10);
    
    tokio::spawn(async move {
        for i in 0..10 {
            let event = format!("data: Message {}\n\n", i);
            tx.send(Ok(Frame::data(Bytes::from(event)))).await.unwrap();
            tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
        }
    });
    
    let stream = ReceiverStream::new(rx);
    let body = StreamBody::new(stream);
    
    let mut response = Response::new(body);
    response.headers_mut().insert(
        header::CONTENT_TYPE,
        header::HeaderValue::from_static("text/event-stream"),
    );
    response.headers_mut().insert(
        header::CACHE_CONTROL,
        header::HeaderValue::from_static("no-cache"),
    );
    response.headers_mut().insert(
        header::CONNECTION,
        header::HeaderValue::from_static("keep-alive"),
    );
    
    Ok(response)
}
 
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
    let listener = TcpListener::bind("127.0.0.1:3000").await?;
    println!("SSE endpoint at http://127.0.0.1:3000/events");
    
    loop {
        let (stream, _) = listener.accept().await?;
        let io = TokioIo::new(stream);
        
        tokio::spawn(async move {
            let _ = http1::Builder::new()
                .serve_connection(io, service_fn(handle_sse))
                .await;
        });
    }
}

Middleware Pattern

use hyper::body::Bytes;
use hyper::server::conn::http1;
use hyper::service::service_fn;
use hyper::{Method, Request, Response, StatusCode, header};
use http_body_util::Full;
use hyper_util::rt::TokioIo;
use tokio::net::TcpListener;
use std::convert::Infallible;
use std::time::Instant;
 
// Logging middleware
async fn log_request(req: &Request<hyper::body::Incoming>) {
    println!("[{}] {} {}", 
        chrono::Local::now().format("%Y-%m-%d %H:%M:%S"),
        req.method(),
        req.uri().path()
    );
}
 
// Auth middleware
fn check_auth(req: &Request<hyper::body::Incoming>) -> bool {
    req.headers()
        .get("Authorization")
        .and_then(|v| v.to_str().ok())
        .map(|s| s == "Bearer secret-token")
        .unwrap_or(false)
}
 
// Timing middleware
async fn with_timing<F, Fut>(req: Request<hyper::body::Incoming>, handler: F) -> Response<Full<Bytes>>
where
    F: FnOnce(Request<hyper::body::Incoming>) -> Fut,
    Fut: std::future::Future<Output = Result<Response<Full<Bytes>>, Infallible>>,
{
    let start = Instant::now();
    let method = req.method().clone();
    let path = req.uri().path().to_string();
    
    let response = handler(req).await;
    
    let elapsed = start.elapsed();
    println!("[TIMING] {} {} - {:?}", method, path, elapsed);
    
    response.unwrap()
}
 
async fn handle_request(req: Request<hyper::body::Incoming>) -> Result<Response<Full<Bytes>>, Infallible> {
    log_request(&req).await;
    
    // Check authentication for protected routes
    if req.uri().path().starts_with("/api/") {
        if !check_auth(&req) {
            let mut res = Response::new(Full::new(Bytes::from("Unauthorized")));
            *res.status_mut() = StatusCode::UNAUTHORIZED;
            return Ok(res);
        }
    }
    
    match (req.method(), req.uri().path()) {
        (&Method::GET, "/") => {
            Ok(Response::new(Full::new(Bytes::from("Public endpoint"))))
        }
        (&Method::GET, "/api/data") => {
            Ok(Response::new(Full::new(Bytes::from("Protected data"))))
        }
        _ => {
            let mut res = Response::new(Full::new(Bytes::from("Not Found")));
            *res.status_mut() = StatusCode::NOT_FOUND;
            Ok(res)
        }
    }
}
 
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
    let listener = TcpListener::bind("127.0.0.1:3000").await?;
    println!("Try:");
    println!("  curl http://127.0.0.1:3000/");
    println!("  curl http://127.0.0.1:3000/api/data");
    println!("  curl -H 'Authorization: Bearer secret-token' http://127.0.0.1:3000/api/data");
    
    loop {
        let (stream, _) = listener.accept().await?;
        let io = TokioIo::new(stream);
        
        tokio::spawn(async move {
            let _ = http1::Builder::new()
                .serve_connection(io, service_fn(handle_request))
                .await;
        });
    }
}

Shared State

use hyper::body::Bytes;
use hyper::server::conn::http1;
use hyper::service::service_fn;
use hyper::{Method, Request, Response, StatusCode};
use http_body_util::Full;
use hyper_util::rt::TokioIo;
use tokio::net::TcpListener;
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
use std::convert::Infallible;
 
struct AppState {
    request_count: AtomicU64,
    start_time: std::time::Instant,
}
 
impl AppState {
    fn new() -> Self {
        Self {
            request_count: AtomicU64::new(0),
            start_time: std::time::Instant::now(),
        }
    }
    
    fn increment_requests(&self) -> u64 {
        self.request_count.fetch_add(1, Ordering::Relaxed)
    }
    
    fn get_request_count(&self) -> u64 {
        self.request_count.load(Ordering::Relaxed)
    }
    
    fn uptime(&self) -> std::time::Duration {
        self.start_time.elapsed()
    }
}
 
async fn handle_request(
    req: Request<hyper::body::Incoming>,
    state: Arc<AppState>,
) -> Result<Response<Full<Bytes>>, Infallible> {
    let count = state.increment_requests();
    
    match (req.method(), req.uri().path()) {
        (&Method::GET, "/stats") => {
            let stats = format!(
                "Requests: {}\nUptime: {:?}",
                state.get_request_count(),
                state.uptime()
            );
            Ok(Response::new(Full::new(Bytes::from(stats))))
        }
        (&Method::GET, "/") => {
            Ok(Response::new(Full::new(Bytes::from(format!(
                "Hello! Request #{}", count + 1
            )))))
        }
        _ => {
            let mut res = Response::new(Full::new(Bytes::from("Not Found")));
            *res.status_mut() = StatusCode::NOT_FOUND;
            Ok(res)
        }
    }
}
 
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
    let listener = TcpListener::bind("127.0.0.1:3000").await?;
    let state = Arc::new(AppState::new());
    
    println!("Server running at http://127.0.0.1:3000");
    println!("Stats at http://127.0.0.1:3000/stats");
    
    loop {
        let (stream, _) = listener.accept().await?;
        let io = TokioIo::new(stream);
        let state = Arc::clone(&state);
        
        tokio::spawn(async move {
            let _ = http1::Builder::new()
                .serve_connection(io, service_fn(move |req| {
                    let state = Arc::clone(&state);
                    async move { handle_request(req, state).await }
                }))
                .await;
        });
    }
}

Graceful Shutdown

use hyper::body::Bytes;
use hyper::server::conn::http1;
use hyper::service::service_fn;
use hyper::{Request, Response};
use http_body_util::Full;
use hyper_util::rt::TokioIo;
use tokio::net::TcpListener;
use tokio::signal;
use std::convert::Infallible;
 
async fn handle_request(_req: Request<hyper::body::Incoming>) -> Result<Response<Full<Bytes>>, Infallible> {
    Ok(Response::new(Full::new(Bytes::from("Hello!"))))
}
 
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
    let listener = TcpListener::bind("127.0.0.1:3000").await?;
    println!("Server running. Press Ctrl+C to shut down.");
    
    tokio::spawn(async {
        signal::ctrl_c().await.expect("Failed to listen for Ctrl+C");
        println!("\nShutting down...");
        std::process::exit(0);
    });
    
    loop {
        let (stream, _) = listener.accept().await?;
        let io = TokioIo::new(stream);
        
        tokio::spawn(async move {
            let _ = http1::Builder::new()
                .serve_connection(io, service_fn(handle_request))
                .await;
        });
    }
}

Error Handling

use hyper::body::Bytes;
use hyper::server::conn::http1;
use hyper::service::service_fn;
use hyper::{Method, Request, Response, StatusCode};
use http_body_util::Full;
use hyper_util::rt::TokioIo;
use tokio::net::TcpListener;
use std::convert::Infallible;
use std::num::ParseIntError;
 
#[derive(Debug)]
enum ApiError {
    NotFound,
    ParseError(ParseIntError),
    Internal(String),
}
 
impl std::fmt::Display for ApiError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            ApiError::NotFound => write!(f, "Not Found"),
            ApiError::ParseError(e) => write!(f, "Parse error: {}", e),
            ApiError::Internal(s) => write!(f, "Internal error: {}", s),
        }
    }
}
 
fn error_response(error: ApiError) -> Response<Full<Bytes>> {
    let (status, message) = match error {
        ApiError::NotFound => (StatusCode::NOT_FOUND, "Resource not found"),
        ApiError::ParseError(_) => (StatusCode::BAD_REQUEST, "Invalid parameter"),
        ApiError::Internal(_) => (StatusCode::INTERNAL_SERVER_ERROR, "Internal server error"),
    };
    
    Response::builder()
        .status(status)
        .body(Full::new(Bytes::from(message.to_string())))
        .unwrap()
}
 
async fn handle_request(req: Request<hyper::body::Incoming>) -> Result<Response<Full<Bytes>>, Infallible> {
    let path = req.uri().path();
    
    let result: Result<Response<Full<Bytes>>, ApiError> = match (req.method(), path) {
        (&Method::GET, path) if path.starts_with("/square/") => {
            let num_str = path.trim_start_matches("/square/");
            let num: i32 = num_str.parse().map_err(ApiError::ParseError)?;
            let result = num * num;
            Ok(Response::new(Full::new(Bytes::from(result.to_string()))))
        }
        (&Method::GET, "/") => {
            Ok(Response::new(Full::new(Bytes::from("Welcome!"))))
        }
        _ => Err(ApiError::NotFound),
    };
    
    Ok(result.unwrap_or_else(error_response))
}
 
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
    let listener = TcpListener::bind("127.0.0.1:3000").await?;
    println!("Try: http://127.0.0.1:3000/square/12");
    
    loop {
        let (stream, _) = listener.accept().await?;
        let io = TokioIo::new(stream);
        
        tokio::spawn(async move {
            let _ = http1::Builder::new()
                .serve_connection(io, service_fn(handle_request))
                .await;
        });
    }
}

Real-World REST API Example

use hyper::body::Bytes;
use hyper::server::conn::http1;
use hyper::service::service_fn;
use hyper::{header, Method, Request, Response, StatusCode};
use http_body_util::{BodyExt, Full};
use hyper_util::rt::TokioIo;
use tokio::net::TcpListener;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use tokio::sync::RwLock;
use std::collections::HashMap;
use std::convert::Infallible;
 
#[derive(Debug, Clone, Serialize, Deserialize)]
struct Todo {
    id: u64,
    title: String,
    completed: bool,
}
 
type TodoStore = Arc<RwLock<HashMap<u64, Todo>>>;
 
#[derive(Deserialize)]
struct CreateTodo {
    title: String,
}
 
fn json_response<T: Serialize>(data: T, status: StatusCode) -> Response<Full<Bytes>> {
    let json = serde_json::to_string(&data).unwrap();
    Response::builder()
        .status(status)
        .header(header::CONTENT_TYPE, "application/json")
        .body(Full::new(Bytes::from(json)))
        .unwrap()
}
 
async fn handle_todos(
    req: Request<hyper::body::Incoming>,
    store: TodoStore,
) -> Result<Response<Full<Bytes>>, Infallible> {
    let path = req.uri().path().to_string();
    let segments: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect();
    
    match (req.method(), segments.as_slice()) {
        // GET /todos - list all
        (&Method::GET, ["todos"]) => {
            let todos = store.read().await;
            let list: Vec<_> = todos.values().cloned().collect();
            Ok(json_response(list, StatusCode::OK))
        }
        
        // GET /todos/:id - get one
        (&Method::GET, ["todos", id]) => {
            let id: u64 = match id.parse() {
                Ok(id) => id,
                Err(_) => return Ok(Response::builder()
                    .status(StatusCode::BAD_REQUEST)
                    .body(Full::new(Bytes::from("Invalid ID")))
                    .unwrap()),
            };
            
            let todos = store.read().await;
            match todos.get(&id) {
                Some(todo) => Ok(json_response(todo.clone(), StatusCode::OK)),
                None => Ok(Response::builder()
                    .status(StatusCode::NOT_FOUND)
                    .body(Full::new(Bytes::from("Todo not found")))
                    .unwrap()),
            }
        }
        
        // POST /todos - create
        (&Method::POST, ["todos"]) => {
            let body = req.collect().await.unwrap().to_bytes();
            let create: CreateTodo = match serde_json::from_slice(&body) {
                Ok(c) => c,
                Err(_) => return Ok(Response::builder()
                    .status(StatusCode::BAD_REQUEST)
                    .body(Full::new(Bytes::from("Invalid JSON")))
                    .unwrap()),
            };
            
            let mut todos = store.write().await;
            let id = todos.keys().max().unwrap_or(&0) + 1;
            let todo = Todo {
                id,
                title: create.title,
                completed: false,
            };
            todos.insert(id, todo.clone());
            
            Ok(json_response(todo, StatusCode::CREATED))
        }
        
        // DELETE /todos/:id
        (&Method::DELETE, ["todos", id]) => {
            let id: u64 = match id.parse() {
                Ok(id) => id,
                Err(_) => return Ok(Response::builder()
                    .status(StatusCode::BAD_REQUEST)
                    .body(Full::new(Bytes::from("Invalid ID")))
                    .unwrap()),
            };
            
            let mut todos = store.write().await;
            match todos.remove(&id) {
                Some(_) => Ok(Response::new(Full::new(Bytes::from("Deleted")))),
                None => Ok(Response::builder()
                    .status(StatusCode::NOT_FOUND)
                    .body(Full::new(Bytes::from("Todo not found")))
                    .unwrap()),
            }
        }
        
        _ => {
            Ok(Response::builder()
                .status(StatusCode::NOT_FOUND)
                .body(Full::new(Bytes::from("Not Found")))
                .unwrap())
        }
    }
}
 
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
    let listener = TcpListener::bind("127.0.0.1:3000").await?;
    let store: TodoStore = Arc::new(RwLock::new(HashMap::new()));
    
    println!("Todo API running at http://127.0.0.1:3000");
    println!("Endpoints:");
    println!("  GET    /todos");
    println!("  GET    /todos/:id");
    println!("  POST   /todos");
    println!("  DELETE /todos/:id");
    
    loop {
        let (stream, _) = listener.accept().await?;
        let io = TokioIo::new(stream);
        let store = Arc::clone(&store);
        
        tokio::spawn(async move {
            let _ = http1::Builder::new()
                .serve_connection(io, service_fn(move |req| {
                    let store = Arc::clone(&store);
                    async move { handle_todos(req, store).await }
                }))
                .await;
        });
    }
}

Summary

  • Use TcpListener to accept connections, then serve_connection for each
  • service_fn wraps a closure that handles RequestResponse
  • Response::builder() for building responses with status and headers
  • BodyExt::collect() to read entire request body
  • Use Arc to share state across handlers
  • Hyper 1.x requires http-body-util and hyper-util crates
  • Stream responses with StreamBody for large or generated content
  • Implement middleware by wrapping handlers
  • Use tokio::spawn to handle each connection concurrently
  • StatusCode for HTTP status codes
  • header module contains standard header names
  • Perfect for: low-level HTTP services, building web frameworks, high-performance APIs, custom protocols