Rust Web Server Tutorial

Introduction

In this tutorial, we’ll build a simple Rust web server from scratch using the actix-web framework. actix-web is one of the most popular and performant web frameworks for Rust, and it makes it easy to get a web server running quickly.

Prerequisites:

  • Basic understanding of Rust programming

  • Rust installed on your machine (rustc and cargo)

  • Basic understanding of HTTP and web servers


Step 1: Set Up Your Rust Project

First, set up a new Rust project using cargo. Open your terminal and run the following command:

bashCopy codecargo new rust-web-server

Navigate into your new project:

bashCopy codecd rust-web-server

This creates a basic Rust project with a src/main.rs file.

Step 2: Add Dependencies

We’ll need to add the actix-web crate to the project. Open Cargo.toml and add the following lines under [dependencies]:

tomlCopy code[dependencies]
actix-web = "4.0"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
  • actix-web will be the main framework for handling HTTP requests.

  • serde is for serializing/deserializing data.

  • serde_json will handle JSON formatting.

Then, update your dependencies by running:

bashCopy codecargo build

Step 3: Create a Simple Web Server

Now, let’s build a simple "Hello, World!" web server. Open src/main.rs and replace its contents with the following:

rustCopy codeuse actix_web::{get, App, HttpResponse, HttpServer, Responder};

#[get("/")]
async fn hello() -> impl Responder {
    HttpResponse::Ok().body("Hello, World!")
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| {
        App::new()
            .service(hello) // Attach the hello route to the web server
    })
    .bind("127.0.0.1:8080")? // Bind the server to a local address
    .run()
    .await
}

Here’s what’s happening:

  • We define a route / using the #[get("/")] attribute.

  • The hello function returns a simple "Hello, World!" response using HttpResponse::Ok().

  • In the main function, we create a new HttpServer, bind it to port 8080, and attach our hello service to the server.

To run the server, use:

bashCopy codecargo run

Now, go to http://127.0.0.1:8080/ in your browser, and you should see "Hello, World!"


Step 4: Adding JSON Support

Next, let’s handle JSON responses. This is useful when building APIs. We’ll define a route that returns JSON data.

Replace the existing code in main.rs with the following:

rustCopy codeuse actix_web::{get, App, HttpResponse, HttpServer, Responder};
use serde::Serialize;

#[derive(Serialize)]
struct MyResponse {
    message: String,
    status: u16,
}

#[get("/json")]
async fn json_response() -> impl Responder {
    let response = MyResponse {
        message: "This is a JSON response".to_string(),
        status: 200,
    };

    HttpResponse::Ok().json(response)
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| {
        App::new()
            .service(json_response) // Add the new JSON route
    })
    .bind("127.0.0.1:8080")?
    .run()
    .await
}

Here’s what changed:

  • We defined a MyResponse struct, deriving Serialize, which allows it to be converted to JSON.

  • In json_response, we create a MyResponse instance and return it as JSON using HttpResponse::Ok().json().

Run the server again, and navigate to http://127.0.0.1:8080/json. You’ll receive a JSON response like:

jsonCopy code{
  "message": "This is a JSON response",
  "status": 200
}

Step 5: Handling POST Requests

Now that we’ve covered GET requests, let’s implement a POST route. We'll create an endpoint that accepts JSON data and responds with a message.

Modify main.rs:

rustCopy codeuse actix_web::{post, web, App, HttpResponse, HttpServer, Responder};
use serde::Deserialize;

#[derive(Deserialize)]
struct InputData {
    name: String,
}

#[post("/echo")]
async fn echo(input: web::Json<InputData>) -> impl Responder {
    let response = format!("Hello, {}!", input.name);
    HttpResponse::Ok().body(response)
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| {
        App::new()
            .service(echo) // POST route
    })
    .bind("127.0.0.1:8080")?
    .run()
    .await
}

Here:

  • We define a POST route with #[post("/echo")].

  • The InputData struct is used to deserialize the incoming JSON body.

  • We extract the JSON from the request using web::Json<InputData> and return a personalized message.

To test this, send a POST request using a tool like curl or Postman:

bashCopy codecurl -X POST http://127.0.0.1:8080/echo -H "Content-Type: application/json" -d '{"name": "Alice"}'

You should get back:

textCopy codeHello, Alice!

Step 6: Handling Errors

Let’s add basic error handling to ensure the server behaves correctly when receiving invalid requests. We’ll modify the POST handler to return an appropriate error response when the JSON format is incorrect.

Here’s how you can modify the echo route to handle errors:

rustCopy codeuse actix_web::{error, post, web, App, HttpResponse, HttpServer, Responder};
use serde::Deserialize;

#[derive(Deserialize)]
struct InputData {
    name: String,
}

#[post("/echo")]
async fn echo(input: web::Json<InputData>) -> Result<impl Responder, actix_web::Error> {
    if input.name.is_empty() {
        return Err(error::ErrorBadRequest("Name cannot be empty"));
    }
    
    let response = format!("Hello, {}!", input.name);
    Ok(HttpResponse::Ok().body(response))
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| {
        App::new()
            .service(echo)
    })
    .bind("127.0.0.1:8080")?
    .run()
    .await
}

Here:

  • We use Result<impl Responder, actix_web::Error> to return either a valid response or an error.

  • If the name field is empty, the server returns a 400 Bad Request error with a custom message.


Step 7: Serving Static Files

Next, let’s add support for serving static files such as HTML, CSS, and JavaScript. This is useful when building full-fledged web applications.

First, create a directory named static in the root of your project and add a file called index.html inside:

htmlCopy code<!-- static/index.html -->
<!DOCTYPE html>
<html>
<head>
    <title>Rust Web Server</title>
</head>
<body>
    <h1>Welcome to the Rust Web Server</h1>
</body>
</html>

Now, modify main.rs to serve this static file:

rustCopy codeuse actix_files as fs;
use actix_web::{App, HttpServer};

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| {
        App::new()
            .service(fs::Files::new("/", "./static").index_file("index.html"))
    })
    .bind("127.0.0.1:8080")?
    .run()
    .await
}

Here:

  • We use actix_files::Files to serve files from the static directory.

  • The index_file method specifies index.html as the default file to serve when accessing the root.

Now when you navigate to http://127.0.0.1:8080/, you’ll see the index.html content.


Step 8: Adding Middleware

Middleware allows you to modify requests and responses globally. For example, you might want to log every incoming request or add security headers.

Let’s add a simple logger middleware:

Modify main.rs:

rustCopy codeuse actix_web::{middleware, App, HttpServer};

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| {
        App::new()
            .wrap(middleware::Logger::default()) // Add logger middleware
            .service(fs::Files::new("/", "./static").index_file("index.html"))
    })
    .bind("127.0.0.1:8080")?
    .run()
    .await
}
  • The middleware::Logger::default() middleware logs each request.

  • You can customize it by passing a format string to Logger::new().

Make sure to enable actix-web’s logger by setting the environment variable:

bashCopy codeRUST_LOG=actix_web=info cargo run

Conclusion

We’ve built a simple web server using Rust and actix-web, covering essential web server features like routing, JSON handling, error handling, serving static files, and adding middleware. From here, you can explore more advanced features like databases, websockets, or authentication with actix-web.

This should give you a strong foundation for building web applications with Rust!

Last updated