Building a Simple Web API in Rust with Actix Web

Rust is a powerful language known for its speed and memory safety, but its strict type system and borrow checker can be challenging, especially for those coming from JavaScript or C++. When exploring web development with Rust, I found a lack of beginner-friendly resources, particularly for Actix Web.

When I first attempted to build a web backend in Rust, I underestimated the complexity. Between the borrow checker and the scarcity of beginner-oriented Actix Web tutorials, the learning curve was steep. This guide aims to provide a structured approach to setting up an Actix Web backend, covering essential concepts and API development.


Scope of the Article

We'll cover:

  1. Setting up a Rust project for Actix Web
  2. Writing a simple API with route handling
  3. Running and testing the server
  4. Understanding the project structure and key concepts

Framework and Tools Used

  • Actix Web – The Rust web framework we’ll use for handling HTTP requests
  • Cargo – The Rust package manager for managing dependencies
  • Rust (Stable Version) – Make sure you have Rust installed before proceeding
  • Postman – We will use Postman to test the API rather than curl.

Now, let's dive in and build our Actix Web backend.


1. Creating the Project

Open your terminal and run:

cargo new actix-web-demo --bin

This creates a new Rust project named actix-web-demo with a binary (--bin) target. Now, navigate into the project directory:

cd actix-web-demo

2. Adding Dependencies

Open the Cargo.toml file and add actix-web as a dependency:

[dependencies]
actix-web = "4"

Then, run:

cargo build

This downloads and compiles the dependencies, ensuring everything is set up correctly.


3. Writing a Simple API with Route Handling

Open src/main.rs and replace its contents with the following:

use actix_web::{web, App, HttpResponse, HttpServer, Responder};

// A simple handler function that returns a welcome message
async fn greet() -> impl Responder {
    HttpResponse::Ok().body("Hello from Actix Web!")
}

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

This creates a basic Actix Web server with a route (/) that maps GET requests to greet().


4. Running and Testing the Server

Run:

cargo run

Now, open your browser and navigate to 127.0.0.1:8080. You should see the message:

Hello from Actix Web!

Testing with Postman

Now that the server is running, let's test the API using Postman instead of curl.

  1. Open Postman

    If you haven't already, download and install Postman.

  2. Set up the request in Postman

    • Method: GET
    • URL: http://127.0.0.1:8080/
    • Click "Send"
    • Expected Result: You should see the response Hello from Actix Web!.

5. Adding More Routes

We’ll extend this API by adding multiple routes that return dummy JSON data. Our endpoints will include:

  1. GET /users – Returns a list of users
  2. GET /users/{id} – Fetches a single user by ID
  3. POST /users – Creates a new user (dummy response)
  4. DELETE /users/{id} – Deletes a user (dummy response)

6. Updating Dependencies

Modify Cargo.toml to include serde and serde_json for JSON parsing:

[dependencies]
actix-web = "4"
serde = { version = "1", features = ["derive"] }
serde_json = "1"

Run:

cargo build

7. Writing the API

Modify src/main.rs:

use actix_web::{web, App, HttpResponse, HttpServer, Responder};
use serde::{Deserialize, Serialize};

// Struct to represent a user
#[derive(Serialize, Deserialize)]
struct User {
    id: u32,
    name: String,
    email: String,
}

// Dummy user data
fn get_dummy_users() -> Vec<User> {
    vec![
        User { id: 1, name: "Alice".into(), email: "alice@example.com".into() },
        User { id: 2, name: "Bob".into(), email: "bob@example.com".into() },
    ]
}

// GET /users - Returns all users
async fn get_users() -> impl Responder {
    HttpResponse::Ok().json(get_dummy_users())
}

// GET /users/{id} - Fetch user by ID
async fn get_user_by_id(path: web::Path<u32>) -> impl Responder {
    let user_id = path.into_inner();
    let users = get_dummy_users();

    if let Some(user) = users.into_iter().find(|u| u.id == user_id) {
        HttpResponse::Ok().json(user)
    } else {
        HttpResponse::NotFound().body("User not found")
    }
}

// POST /users - Create a new user (dummy response)
#[derive(Deserialize)]
struct NewUser {
    name: String,
    email: String,
}

async fn create_user(user: web::Json<NewUser>) -> impl Responder {
    let new_user = User {
        id: 3, // Dummy ID
        name: user.name.clone(),
        email: user.email.clone(),
    };

    HttpResponse::Created().json(new_user)
}

// DELETE /users/{id} - Delete a user (dummy response)
async fn delete_user(path: web::Path<u32>) -> impl Responder {
    let user_id = path.into_inner();
    HttpResponse::Ok().body(format!("User with ID {} deleted", user_id))
}

// Main function to set up the Actix Web server
#[actix_web::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| {
        App::new()
            .route("/users", web::get().to(get_users))
            .route("/users/{id}", web::get().to(get_user_by_id))
            .route("/users", web::post().to(create_user))
            .route("/users/{id}", web::delete().to(delete_user))
    })
    .bind("127.0.0.1:8080")?
    .run()
    .await
}

8. Running and Testing the API

Start the server:

cargo run

Test Endpoints Using Postman

1. Get All Users

  • Method: GET
  • URL: http://127.0.0.1:8080/users
  • Click "Send"
  • Expected Result: You should see a list of users in JSON format.

2. Get a User by ID

  • Method: GET
  • URL: http://127.0.0.1:8080/users/1 (or any other ID)
  • Click "Send"
  • Expected Result: You should see the user with the specified ID or a "User not found" message.

3. Create a New User

  • Method: POST
  • URL: http://127.0.0.1:8080/users
  • Body:

    • Select raw and set the type to JSON.
    • Add the following JSON:
    {
      "name": "Charlie",
      "email": "charlie@example.com"
    }
    
  • Click "Send"

  • Expected Result: You should see a response with the newly created user, including a dummy ID.

4. Delete a User

  • Method: DELETE
  • URL: http://127.0.0.1:8080/users/2 (replace 2 with the ID of the user you want to delete)
  • Click "Send"
  • Expected Result: You should receive a message like "User with ID 2 deleted".

Conclusion

Now we have a simple CRUD-style API in Rust using Actix Web. From here, you can:

  • Connect it to a real database like PostgreSQL using Diesel
  • Improve error handling with Result
  • Implement authentication with JWT

Rust’s strictness might feel frustrating at first, but once you get through it, the speed and safety make it worth the effort. With Postman, testing and interacting with your API is more intuitive, and you can expand the API with additional functionality or features as needed.