Rust REST APIs

Why Use Rust for REST APIs?

Rust has quickly become one of the most powerful languages available today. It combines the speed of low-level languages with the safety and simplicity of modern programming.

1) High Performance: REST APIs often need to handle thousands of client requests every second, and speed matters. Rust compiles directly to machine code and runs as fast as C or C++, allowing your API to respond quickly even under heavy load.

2) Memory Safety: One of Rust’s biggest strengths is that it provides memory safety at compile time.

3) Asynchronous Programming: Modern web applications require APIs that can process thousands of concurrent connections efficiently. Rust’s async/await system, combined with frameworks like Tokio, makes it easy to write highly concurrent and non-blocking code.

4) Robust Ecosystem: Frameworks like Actix Web and Axum simplify API development.

Getting Started with REST APIs in Rust

Once you know why Rust is a great choice for backend development, the next step is to learn how to actually build a REST API.

Rust may look complex at first, but with the right tools and a step-by-step approach, you can create a fully working API in minutes.

To build a REST API in Rust, you’ll mainly use three types of tools:

1) Framework: A framework like Actix Web or Axum helps you manage routing (URLs), handle requests (like GET, POST), and manage middleware easily.

2) Data Handling: REST APIs often send and receive JSON data. The serde crate makes it simple to convert between JSON and Rust structs.

3) Database (Optional): If your API needs to store or retrieve data, use libraries like sqlx or diesel to connect with databases such as PostgreSQL or MySQL.

1. Create a New Rust Project

First, let’s start fresh by creating a new project:

cargo new rust-rest-api
cd rust-rest-api

Now, open the Cargo.toml file and add these dependencies:

[dependencies]
actix-web = "4.0"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
tokio = { version = "1", features = ["full"] }

Run the following command to install dependencies:

cargo build

2. Build Your First REST API Endpoint

Let’s write a simple REST API that responds to a GET request at the root path (/). Create a new file src/main.rs and add this code:

Example: Simple GET Request

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

/// This handler runs when someone visits GET /
async fn home() -> impl Responder {
HttpResponse::Ok().body("Hello from your first Rust REST API!")
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
println!("Server starting at http://127.0.0.1:8080");

HttpServer::new(|| {
App::new()
// Register a GET route for "/"
.route("/", web::get().to(home))
})
.bind("127.0.0.1:8080")? // Bind the server to localhost:8080
.run()
.await
}
  • Explanation:
    • HttpServer::new starts the HTTP server.
    • App::new() builds your API application.
    • .route(“/”, web::get().to(home)) registers a route: whenever a GET request hits /, the home() function is called.
    • HttpResponse::Ok().body(“…”) sends back an HTTP 200 OK response with a message in the body.

Run the server:

cargo run

Visit http://127.0.0.1:8080 in your browser to see the message.

3. Adding JSON Responses

Example: Returning JSON from an API

For example, instead of just sending “Hello World”, a proper API returns something like:

{
"message": "Hello, Rust REST API!"
}

Let’s see how to do this in Rust with a simple example.

Return JSON from a GET API

Here’s a small Rust program that sends a JSON response when someone visits the /api/message endpoint:

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

/// Step 1: Create a struct that represents the JSON data structure.
/// `Serialize` allows Rust to convert this struct into JSON automatically.
#[derive(Serialize)]
struct ApiResponse {
status: String,
message: String,
}

/// Step 2: Define a route handler that returns JSON.
/// Instead of returning a simple string, we return `web::Json(...)`.
async fn get_api_message() -> impl Responder {
let response = ApiResponse {
status: "success".to_string(),
message: "JSON response working perfectly!".to_string(),
};

// Wrap the struct in `web::Json` so Actix automatically converts it to JSON
web::Json(response)
}

/// Step 3: Start the server and register the route.
#[actix_web::main]
async fn main() -> std::io::Result<()> {
println!("JSON API running on http://127.0.0.1:8080/api/message");

HttpServer::new(|| {
App::new()
// Register the /api/message GET route
.route("/api/message", web::get().to(get_api_message))
})
.bind("127.0.0.1:8080")?
.run()
.await
}
  • Explanation:
    • #[derive(Serialize)]: Converts the struct into JSON.
    • web::Json: Wraps the struct in a JSON response.

Visit http://127.0.0.1:8080/json to see:

{"message":"Hello, Rust REST API!"}

You’ll see the JSON response:

{
"status": "success",
"message": "JSON response working perfectly!"
}

4. Implementing CRUD Operations

In any real-world application, whether it’s a blog, an e-commerce store, or a user management system, you’ll always need CRUD:

  • Create – Add new data (like adding a new user).
  • Read – Fetch existing data.
  • Update – Modify existing data.
  • Delete – Remove data (like deleting a user).

Let’s build a simple User API using Actix Web that does all four operations:

Example: CRUD for a User Resource

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

/// Step 1: Define a User structure.
/// - `Serialize` and `Deserialize` allow us to convert between JSON and Rust structs.
/// - `Clone` helps us return user data safely.
#[derive(Serialize, Deserialize, Clone)]
struct User {
id: u32,
name: String,
}

/// Step 2: Define shared application state.
/// We’ll store all users inside a vector protected by a `Mutex` for thread safety.
struct AppState {
users: Mutex<Vec<User>>,
}

/// CREATE: Add a new user (POST /users)
async fn create_user(data: web::Data<AppState>, new_user: web::Json<User>) -> impl Responder {
let mut users = data.users.lock().unwrap();
users.push(new_user.into_inner());
HttpResponse::Ok().body("User created successfully!")
}

/// READ: Get all users (GET /users)
async fn get_users(data: web::Data<AppState>) -> impl Responder {
let users = data.users.lock().unwrap();
web::Json(users.clone()) // Convert Vec<User> into JSON response
}

/// UPDATE: Update a user’s name by ID (PUT /users/{id})
async fn update_user(
data: web::Data<AppState>,
path: web::Path<u32>,
updated: web::Json<User>,
) -> impl Responder {
let user_id = path.into_inner();
let mut users = data.users.lock().unwrap();

if let Some(user) = users.iter_mut().find(|u| u.id == user_id) {
user.name = updated.name.clone();
HttpResponse::Ok().body("User updated successfully!")
} else {
HttpResponse::NotFound().body("User not found!")
}
}

/// DELETE: Remove a user by ID (DELETE /users/{id})
async fn delete_user(data: web::Data<AppState>, path: web::Path<u32>) -> impl Responder {
let user_id = path.into_inner();
let mut users = data.users.lock().unwrap();

let original_len = users.len();
users.retain(|u| u.id != user_id);

if users.len() < original_len {
HttpResponse::Ok().body("User deleted successfully!")
} else {
HttpResponse::NotFound().body("User not found!")
}
}

/// MAIN: Start the server and register all routes
#[actix_web::main]
async fn main() -> std::io::Result<()> {
println!("Server running on http://127.0.0.1:8080");

let state = web::Data::new(AppState {
users: Mutex::new(Vec::new()),
});

HttpServer::new(move || {
App::new()
.app_data(state.clone())
.route("/users", web::post().to(create_user))
.route("/users", web::get().to(get_users))
.route("/users/{id}", web::put().to(update_user))
.route("/users/{id}", web::delete().to(delete_user))
})
.bind("127.0.0.1:8080")?
.run()
.await
}
  • Explanation:
    • AppState + Mutex<Vec<User>> – This is our in-memory “database.”
    • Mutex: Ensures thread-safe access to shared data.
    • Routes:
      • POST /users: Adds a new user.
      • GET /users: Retrieves all users.
{ "id": 1, "name": "Alice" }

GET /users (Read): Fetch all users as JSON.

PUT /users/{id} (Update): Update a specific user’s name.
Example Request:

{ "id": 1, "name": "Alice Updated" }

DELETE /users/{id} (Delete): Remove a user by ID.

Learn More About Rust Programming

Leave a Comment