Building a Simple Web API with Rust and Actix Web

Last updated: April 13, 2025

1. Introduction: Rust for Web APIs

Rust's focus on performance, reliability, and memory safety makes it an increasingly attractive choice for building backend systems, including web APIs. While newer to the web scene than languages like Node.js, Python, or Go, Rust boasts a growing ecosystem of powerful web frameworks.

This tutorial demonstrates how to build a simple RESTful web API using Rust and one of its most popular asynchronous web frameworks: Actix Web. We'll cover setting up the project, defining routes, handling requests, and returning JSON data.

If you're new to Rust, check out our Getting Started with Rust guide first.

2. Why Actix Web?

Actix Web is a high-performance, pragmatic web framework for Rust. Key features include:

  • Performance: Consistently ranks among the fastest web frameworks in benchmarks.
  • Asynchronous: Built on top of Tokio, Rust's asynchronous runtime, allowing efficient handling of concurrent requests.
  • Actor Model (Optional): Based on the Actix actor framework, though direct use of actors is not required for basic web development.
  • Type Safety: Leverages Rust's strong type system.
  • Extensibility: Supports middleware, WebSockets, and flexible request handling.

While other frameworks like Rocket, Axum, or Warp exist, Actix Web provides a good balance of performance, features, and relative ease of use for building APIs.

3. Project Setup with Cargo

First, create a new Rust binary project using Cargo (Rust's package manager):

cargo new simple_api
cd simple_api

Next, add Actix Web as a dependency in your Cargo.toml file:

[package]
name = "simple_api"
version = "0.1.0"
edition = "2021"

[dependencies]
actix-web = "4" # Use the latest version 4.x

You can also add it via the command line:

cargo add actix-web

4. Creating a Basic "Hello World" Server

Let's start with the simplest possible Actix Web server. Replace the contents of src/main.rs with the following:

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

// Define an asynchronous handler function for the root route "/"
#[get("/")]
async fn hello() -> impl Responder {
    HttpResponse::Ok().body("Hello world!")
}

// The main function needs to be async because HttpServer::run() is async
#[actix_web::main] // Macro to setup the Actix async runtime
async fn main() -> std::io::Result<()> {
    println!("🚀 Server starting at http://127.0.0.1:8080");

    // Create and run the HTTP server
    HttpServer::new(|| {
        // App::new() creates an application instance
        App::new().service(hello) // Register the handler function as a service
    })
    .bind(("127.0.0.1", 8080))? // Bind to address and port
    .run() // Run the server (this is an awaitable future)
    .await
}

Key points:

  • We import necessary types from actix_web.
  • #[get("/")] is an attribute macro that defines an HTTP GET route for the path /.
  • hello is an async handler function. Actix Web handlers are typically asynchronous.
  • impl Responder indicates the function returns something that Actix can convert into an HTTP response. HttpResponse::Ok().body(...) creates a 200 OK response with the specified body.
  • #[actix_web::main] is a macro that sets up the necessary asynchronous runtime (Tokio by default) to run our server.
  • HttpServer::new(|| App::new()... ) configures the server, defining the routes and services it will handle.
  • .bind()? attempts to bind the server to the specified address and port. The ? handles potential errors.
  • .run().await starts the server and keeps it running asynchronously.

Run this using cargo run. You should see the "Server starting..." message. Open your browser or use curl to access http://127.0.0.1:8080/, and you should receive "Hello world!".

5. Defining Routes and Handlers

Actix Web offers several ways to define routes.

  • Attribute Macros (Recommended): #[get("/path")], #[post("/path")], #[put("/path")], #[delete("/path")] directly above handler functions.
  • web::route(): Manually configure routes within the App::new().configure(...) setup.

Let's add another route:

// Add this handler function
#[get("/ping")]
async fn ping() -> impl Responder {
    HttpResponse::Ok().body("pong")
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    println!("🚀 Server starting at http://127.0.0.1:8080");
    HttpServer::new(|| {
        App::new()
            .service(hello)
            .service(ping) // Register the new ping service
    })
    .bind(("127.0.0.1", 8080))?
    .run()
    .await
}

Re-run with cargo run and navigate to http://127.0.0.1:8080/ping to see "pong".

6. Returning JSON Responses

APIs commonly return data in JSON format. Actix Web uses the serde crate for serialization/deserialization.

6.1 Adding Serde Dependency

Add serde to your Cargo.toml, enabling the derive feature:

[dependencies]
actix-web = "4"
serde = { version = "1.0", features = ["derive"] } # Add serde

Or use the command line:

cargo add serde --features derive

6.2 Defining a Response Struct

Create a Rust struct that represents the data you want to return. Use serde::Serialize to make it serializable to JSON.

// Add this near the top
use serde::Serialize;

#[derive(Serialize)] // Derive the Serialize trait
struct Message {
    status: String,
    message: String,
}

6.3 Implementing the JSON Handler

Create a handler that returns an instance of your struct using web::Json.

// Add this handler function
#[get("/status")]
async fn status() -> impl Responder {
    // Create an instance of the Message struct
    let response = Message {
        status: "OK".to_string(),
        message: "Server is healthy".to_string(),
    };
    // Use web::Json to automatically serialize the struct and set Content-Type
    HttpResponse::Ok().json(response)
}

// --- Remember to register the service in main ---
#[actix_web::main]
async fn main() -> std::io::Result<()> {
     println!("🚀 Server starting at http://127.0.0.1:8080");
     HttpServer::new(|| {
         App::new()
             .service(hello)
             .service(ping)
             .service(status) // Register the status service
     })
     .bind(("127.0.0.1", 8080))?
     .run()
     .await
}

Run the server again. Accessing http://127.0.0.1:8080/status will now return a JSON response like:

{
  "status": "OK",
  "message": "Server is healthy"
}

7. Handling Path Parameters

You can extract values from the URL path.

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

// ... (Keep Message struct and other handlers) ...

// Handler that takes a path parameter `name`
#[get("/greet/{name}")] // Define parameter in curly braces
async fn greet(name: web::Path) -> impl Responder {
    // `name` is automatically extracted and deserialized into a String
    let user_name = name.into_inner(); // Get the String value
    HttpResponse::Ok().body(format!("Hello, {}!", user_name))
}


// --- Remember to register the service in main ---
#[actix_web::main]
async fn main() -> std::io::Result<()> {
     println!("🚀 Server starting at http://127.0.0.1:8080");
     HttpServer::new(|| {
         App::new()
             .service(hello)
             .service(ping)
             .service(status)
             .service(greet) // Register the greet service
     })
     .bind(("127.0.0.1", 8080))?
     .run()
     .await
}

Now, if you access http://127.0.0.1:8080/greet/Alice, the response will be "Hello, Alice!".

8. Running and Testing the API

  • Run: Use cargo run in your terminal.
  • Test: Use tools like curl, Postman, Insomnia, or your web browser to interact with the endpoints:
    curl http://127.0.0.1:8080/
    curl http://127.0.0.1:8080/ping
    curl http://127.0.0.1:8080/status
    curl http://127.0.0.1:8080/greet/Bob

9. Conclusion and Next Steps

You've successfully built a basic web API using Rust and Actix Web! We covered setting up the project, defining routes with handlers, returning plain text and JSON responses, and handling path parameters.

This is just the starting point. Next steps could include:

  • Handling different HTTP methods (POST, PUT, DELETE).
  • Extracting query parameters and request bodies.
  • Connecting to a database (using crates like sqlx or diesel).
  • Implementing error handling.
  • Adding middleware for logging, authentication, etc.
  • Structuring your application using modules.

Actix Web and the Rust ecosystem provide the tools to build robust, high-performance web services. While the learning curve might be steeper than some other languages/frameworks, the benefits in safety and speed can be significant.

10. Additional Resources

Related Articles

External Resources