Our server will use a TCP listener to await connections and a simple loop to process them.
Set up a new rust project and open up src/main.rs and add the following:
use std::net::{TcpListener, TcpStream};
use std::io::{Read, Write};
fn main() {
// 1. Bind to a port
let listener = TcpListener::bind("127.0.0.1:7878").unwrap();
println!("Server listening on http://127.0.0.1:7878");
// 2. Loop to accept incoming connections
for stream in listener.incoming() {
let stream = stream.unwrap();
// 3. Handle the connection in a separate function
handle_connection(stream);
}
}
fn handle_connection(mut stream: TcpStream) {
// A buffer to hold the incoming request data
let mut buffer = [0; 1024];
stream.read(&mut buffer).unwrap();
// Convert the buffer slice to a String for inspection
let request = String::from_utf8_lossy(&buffer[..]);
println!("Request: {}", request);
// Placeholder for sending a response (we'll improve this)
let response = "HTTP/1.1 200 OK\r\n\r\nHello, World!";
stream.write(response.as_bytes()).unwrap();
stream.flush().unwrap();
}
Now, run the server: cargo run. Visit http://127.0.0.1:7878 in your browser. You’ll see “Hello, World!” and your terminal will print the HTTP request.
Reading the Request Path
To implement custom routing, we need to extract the requested path (e.g, / or /hello) from the HTTP request. The request is a string starting with the method and path, like: GET /path HTTP/1.1.
Modify handle_connection to parse this:
// ... (in handle_connection function)
// ... stream.read(&mut buffer).unwrap();
let request = String::from_utf8_lossy(&buffer[..]);
// Get the first line of the request (e.g., "GET /path HTTP/1.1")
let request_line = request.lines().next().unwrap_or("");
// Determine the route and response content
let (status_line, content) = match request_line {
"GET / HTTP/1.1" => {
("HTTP/1.1 200 OK", "<h1>Welcome to the Home Page!</h1>")
},
"GET /hello HTTP/1.1" => {
("HTTP/1.1 200 OK", "<h2>Hello there! This is the /hello route.</h2>")
},
_ => { // Default case for 404 Not Found
("HTTP/1.1 404 NOT FOUND", "<h1>404</h1><p>Page Not Found</p>")
}
};
// ... (Rest of the function)
Outputting Custom Responses for Certain Routes
Now, let’s construct the full HTTP response using our determined status and content, and add the necessary Content Length and Content Type headers for proper rendering in the browser.
Update the response creation in handle_connection:
fn handle_connection(mut stream: TcpStream) {
// ... (request reading logic)
// Determine the route and response content
let (status_line, content) = match request_line {
"GET / HTTP/1.1" => {
("HTTP/1.1 200 OK", "<h1>Welcome to the Home Page!</h1>")
},
"GET /hello HTTP/1.1" => {
("HTTP/1.1 200 OK", "<h2>Hello there! This is the /hello route.</h2>")
},
_ => {
("HTTP/1.1 404 NOT FOUND", "<h1>404</h1><p>Page Not Found</p>")
}
};
let response = format!(
"{}\r\nContent-Length: {}\r\nContent-Type: text/html\r\n\r\n{}",
status_line,
content.len(),
content
);
stream.write(response.as_bytes()).unwrap();
stream.flush().unwrap();
}
Run the server and test http://127.0.0.1:7878/, http://127.0.0.1:7878/hello, and any other route.
Hosting a Directory (Serving HTML Files)
Instead of hardcoding the HTML, we can serve a file, which is how you typically “host a directory.” Create a file named hello.html in your project root:
<!DOCTYPE html>
<html>
<head>
<title>Rust Template File</title>
</head>
<body>
<h1>File Hosted Successfully!</h1>
<p>This content was loaded directly from an HTML file by my awesome Rust app.</p>
</body>
</html>
Now, modify the handle_connection function to read this file when the /file route is requested. We’ll use std::fs::read_to_string.
use std::fs; // Add this to the top of src/main.rs
// ... (in handle_connection function)
// Determine the route and response content
let (status_line, filename) = match request_line {
"GET / HTTP/1.1" => ("HTTP/1.1 200 OK", "index.html"), // Assume an index file
"GET /file HTTP/1.1" => ("HTTP/1.1 200 OK", "hello.html"),
_ => ("HTTP/1.1 404 NOT FOUND", "404.html"), // Use a 404 file
};
// **Read the file content**
let content = fs::read_to_string(filename).unwrap_or_else(|_| {
// Fallback content if the file can't be found (e.g., 404.html might not exist yet)
"<h1>Error</h1><p>Could not read file.</p>".to_string()
});
let response = format!(
"{}\r\nContent-Length: {}\r\nContent-Type: text/html\r\n\r\n{}",
status_line,
content.len(),
content
);
// ... (rest of the response sending)
By reading hello.html and using its content in the response, you’ve implemented basic static file serving! While this simplified approach blocks the server for each request, it demonstrates the core concept of reading requests, routing based on the path, and writing responses using only Rust’s standard library. For production use, you’d move to asynchronous solutions or use frameworks like Actix Web or Axum.


