What comes to mind when you think of building a web server with Rust? I bet you think of using Axum, Rocket, Actix, etc., right? These are mature frameworks for building web applications with Rust.
By the end of this article, we’ll learn how to build a web server of our own that will receive a get or post request and respond to it without any of those shiny frameworks.
Prerequisites
Before we begin, make sure you have Rust installed on your system. You can download and install Rust by following the instructions on the official Rust website.
Getting Started
Run mkdir webserver & cd webserver
to create a new directory and navigate into it and cargo init --bin
to initialize a new Rust project with Cargo.
We’ll use the features in the Rust standard library TcpListener
and TcpStream
to power our web server.
The TcpStream
is an implementation that allows you to create a TCP connection in your Rust app. While the TcpListener
allows your application to wait for incoming TCP connections to your server.
So, we’ll first import those two packages and then we’ll also import the Rust Read, Write module as well to enable us to read and write to the TCP stream. So, the imports will look like so:
use std::net::{TcpListener, TcpStream};
use std::io::{Read, Write};
Next, we’ll create the stream handler. So, that when the request is received we can read and write back to the stream.
fn handle_stream(mut stream: TcpStream) {
let mut buffer = [0; 1024];
stream.read(&mut buffer).unwrap();
let response = r#"HTTP/1.1 200 OK
Content-Type: application/json
{"message": "Hello from Rust Web Server!", "status": "success"}"#;
stream.write(response.as_bytes()).unwrap();
stream.flush().unwrap();
}
Let’s explain what is going on here a little bit.
The function receives a TCP Stream as an argument. This is the stream that will be read and written to.
let mut buffer = [0; 1024];
: is a mutable array with a fixed size of 1024, with each item of the array initialized with 0 – 1024 is a reasonable size that should be enough for most requests. This buffer will be used to store data read from the incoming network stream.
Next, we read the stream into the buffer stream.read(&mut buffer).unwrap();
Now, that we’ve got the stream, let’s create a response we’ll return to the client.
let response = r#"HTTP/1.1 200 OK
Content-Type: application/json
{"message": "Hello from Rust Web Server!", "status": "success"}"#;
Finally, we’ll write the response to the stream and flush the stream — Flushing helps avoid any data being stuck in the buffer.
stream.write(response.as_bytes()).unwrap();
stream.flush().unwrap();
Now, we have a clear understanding of what our handler will look like. Note that this will default to handling only GET requests. We’ll see how to implement both POST and GET requests later on in this guide.
Now, let’s implement the main function. Add the following function to the main.rs
file as shown below:
fn main() {
let listener = TcpListener::bind("127.0.0.1:8080").expect("Failed to bind to address");
for stream in listener.incoming() {
match stream {
Ok(stream) => {
std::thread::spawn(|| {
handle_client(stream);
});
}
Err(e) => {
eprintln!("Error: {}", e);
}
}
}
}
In the code above, we spun up the TCP listener and bound it to an IP address and port. We then go ahead to loop over the incoming connections to handle them individually to handle incoming connections. For each successful incoming connection (Ok(stream)
), a new thread is spawned to handle the client. The spawn
function from the std::thread
module is used to create a new thread that runs the code inside the closure:
(|| {
handle_client(stream);
});
And that’s all there is to it.
We have our shiny little server.
Run cargo run
on your Terminal and make a GET request on Postman or visit 127.0.0.1:8080 on your browser. The response should look like this:
Handling POST and GET request
To handle a POST request, we’ll have to update the handler function to handle both GET and Post requests differently. Update the handler function with this code:
fn handle_client(mut stream: TcpStream) {
let mut buffer = [0; 1024];
let bytes_read = stream.read(&mut buffer).unwrap();
let request = String::from_utf8_lossy(&buffer[..bytes_read]);
if request.starts_with("GET /") {
// Handling a GET request
let response = "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\n\r\n{\"message\": \"Hello from Rust Web Server!\"}";
stream.write(response.as_bytes()).unwrap();
} else if request.starts_with("POST /") {
// Handling a POST request
if let Some(body_start) = request.find("\r\n\r\n") {
let body = &request[(body_start + 4)..];
// handle POST request body
// Process the received data and generate a response
let response = format!(
"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\n\r\n{}", body
);
stream.write(response.as_bytes()).unwrap();
}
}
stream.flush().unwrap();
}
Let’s get an understanding of the new improvements we added. We introduced a request variable
let request = String::from_utf8_lossy(&buffer[..bytes_read]);
Let’s again, break down each part of it for a better understanding of what is going on there:
&buffer[..bytes_read]
: This takes a slice of thebuffer
array containing the bytes that were actually read from theTcpStream
. Thebytes_read
variable holds the number of bytes read from the stream. We need to do this so that we can focus only on the part of the buffer with the data that was read otherwise, we’ll get some unwanted characters.String::from_utf8_lossy(...)
: This function converts a sequence of bytes (&[u8]
) into aString
, while handling any invalid UTF-8 sequences by replacing them with the Unicode replacement character (�).
And then finally we use the request.starts_with("GET /")
to get the type of request and process it accordingly. That’s all we need to do to allow both a POST and a GET request.
Go ahead and try the POST request. The response should look like this:
Conclusion
Congratulations! You’ve successfully built a basic web server in Rust. This tutorial covered how to handle client connections, read and write data, create HTTP responses, and set up a server to listen for incoming connections. This is just the beginning of what you can achieve with Rust’s powerful capabilities in networking and systems programming. Feel free to explore further and build more advanced web servers and applications. You can find the full code on GitHub.
Disclaimer: This is not suitable for a real-world application, if you are looking to build something real, you should use a framework like Axum, Actix, or Rocket.
Happy hacking!