Building a Simple Web Server in Rust
As a JavaScript developer, you’re likely familiar with creating web servers using Node.js and Express. In this tutorial, we’ll build a simple HTTP server in Rust using the popular Actix Web framework, and compare it to a Node.js implementation.
Node.js Implementation
Section titled “Node.js Implementation”First, let’s look at how you’d build a simple server with Express:
// server.js
const express = require('express');
const app = express();
const port = 3000;
// JSON middleware
app.use(express.json());
// In-memory "database"
let todos = [
{ id: 1, title: 'Learn Express', completed: true },
{ id: 2, title: 'Learn Rust', completed: false }
];
// Routes
app.get('/todos', (req, res) => {
res.json(todos);
});
app.get('/todos/:id', (req, res) => {
const id = parseInt(req.params.id);
const todo = todos.find(todo => todo.id === id);
if (!todo) {
return res.status(404).json({ error: 'Todo not found' });
}
res.json(todo);
});
app.post('/todos', (req, res) => {
const { title } = req.body;
if (!title) {
return res.status(400).json({ error: 'Title is required' });
}
const newId = todos.length > 0 ? Math.max(...todos.map(t => t.id)) + 1 : 1;
const newTodo = { id: newId, title, completed: false };
todos.push(newTodo);
res.status(201).json(newTodo);
});
app.listen(port, () => {
console.log(`Server running at http://localhost:${port}`);
});
Rust Implementation
Section titled “Rust Implementation”Now, let’s build the same server in Rust using Actix Web.
Step 1: Create a New Rust Project
Section titled “Step 1: Create a New Rust Project”cargo new todo_server
cd todo_server
Step 2: Add Dependencies to Cargo.toml
Section titled “Step 2: Add Dependencies to Cargo.toml”Edit your Cargo.toml
file to include the necessary dependencies:
[package]
name = "todo_server"
version = "0.1.0"
edition = "2021"
[dependencies]
actix-web = "4.3.0"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
once_cell = "1.17.0"
Step 3: Implement the Server
Section titled “Step 3: Implement the Server”Now, let’s implement our server in src/main.rs
:
//main.rs
use actix_web::{web, App, HttpResponse, HttpServer, Responder, get, post};
use once_cell::sync::Lazy;
use serde::{Deserialize, Serialize};
use std::sync::Mutex;
// Todo struct
#[derive(Debug, Serialize, Deserialize, Clone)]
struct Todo {
id: u32,
title: String,
completed: bool,
}
// Create request struct
#[derive(Debug, Deserialize)]
struct CreateTodoRequest {
title: String,
}
// In-memory "database" using a global variable with Mutex for safe concurrent access
static TODOS: Lazy<Mutex<Vec<Todo>>> = Lazy::new(|| {
Mutex::new(vec![
Todo { id: 1, title: "Learn Actix".to_string(), completed: true },
Todo { id: 2, title: "Learn Rust".to_string(), completed: false },
])
});
// Get all todos
#[get("/todos")]
async fn get_todos() -> impl Responder {
let todos = TODOS.lock().unwrap().clone();
HttpResponse::Ok().json(todos)
}
// Get a specific todo by ID
#[get("/todos/{id}")]
async fn get_todo(path: web::Path<u32>) -> impl Responder {
let id = path.into_inner();
let todos = TODOS.lock().unwrap();
match todos.iter().find(|t| t.id == id) {
Some(todo) => HttpResponse::Ok().json(todo),
None => HttpResponse::NotFound().json(("error", "Todo not found")),
}
}
// Create a new todo
#[post("/todos")]
async fn create_todo(req: web::Json<CreateTodoRequest>) -> impl Responder {
let mut todos = TODOS.lock().unwrap();
// Validate request
if req.title.is_empty() {
return HttpResponse::BadRequest().json(("error", "Title is required"));
}
// Generate new ID
let new_id = todos.iter().map(|t| t.id).max().unwrap_or(0) + 1;
// Create new todo
let new_todo = Todo {
id: new_id,
title: req.title.clone(),
completed: false,
};
// Add to collection
todos.push(new_todo.clone());
HttpResponse::Created().json(new_todo)
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
println!("Server running at http://localhost:3000");
HttpServer::new(|| {
App::new()
.service(get_todos)
.service(get_todo)
.service(create_todo)
})
.bind("127.0.0.1:3000")?
.run()
.await
}
Step 4: Run the Server
Section titled “Step 4: Run the Server”cargo run
Your server should now be running at http://localhost:3000.
Key Differences Between Node.js and Rust Implementations
Section titled “Key Differences Between Node.js and Rust Implementations”Type Safety
Section titled “Type Safety”- JavaScript: Dynamic typing means you might encounter runtime type errors.
- Rust: Static typing means the compiler catches type errors before your code runs.
// In Rust, you define the exact structure of your data:
#[derive(Debug, Serialize, Deserialize, Clone)]
struct Todo {
id: u32,
title: String,
completed: bool,
}
Concurrency
Section titled “Concurrency”- JavaScript: Single-threaded event loop with asynchronous callbacks.
- Rust: Multi-threaded by default with the async/await pattern.
// Rust's HTTP server runs multiple threads by default
HttpServer::new(|| {...})
.bind("127.0.0.1:3000")?
.run()
.await
Data Handling
Section titled “Data Handling”- JavaScript: Mutable objects without explicit thread safety.
- Rust: Uses
Mutex
to ensure thread-safe access to shared data.
// Thread-safe global state in Rust
static TODOS: Lazy<Mutex<Vec<Todo>>> = Lazy::new(|| {
Mutex::new(vec![...])
});
Error Handling
Section titled “Error Handling”- JavaScript: Uses exceptions and try/catch.
- Rust: Uses the Result type and pattern matching.
// Rust's ? operator for error handling
.bind("127.0.0.1:3000")?
Performance
Section titled “Performance”The Rust version will generally:
- Have lower memory usage
- Have faster response times, especially under load
- Handle more concurrent connections
Testing the API
Section titled “Testing the API”With curl
Section titled “With curl”# Get all todos
curl http://localhost:3000/todos
# Get a specific todo
curl http://localhost:3000/todos/1
# Create a new todo
curl -X POST http://localhost:3000/todos \
-H "Content-Type: application/json" \
-d '{"title": "Learn Actix Web"}'
Taking It Further
Section titled “Taking It Further”Here are some improvements you could make to this server:
- Add PUT and DELETE routes for updating and deleting todos
- Implement persistence with a database like PostgreSQL using diesel or sqlx
- Add authentication and user management
- Implement middleware for logging, CORS, etc.
- Add error handling middleware
Key Takeaways
Section titled “Key Takeaways”- Rust is more verbose: You need to define data structures explicitly.
- Rust is more type-safe: The compiler catches errors that would only be found at runtime in JavaScript.
- Rust has better concurrency: Multi-threading comes out of the box.
- Rust is faster: Both in execution time and resource usage.
Comparison with JavaScript Ecosystem
Section titled “Comparison with JavaScript Ecosystem”Feature | JavaScript/Node.js | Rust |
---|---|---|
Web frameworks | Express, Koa, Fastify | Actix Web, Rocket, Warp |
Request routing | Simple function-based | Macro-based or builder pattern |
Middleware | Function-based middleware | Trait-based or function middleware |
Performance | Good for I/O bound tasks | Excellent for CPU and I/O bound tasks |
Learning curve | Gentle | Steep |
Next Steps
Section titled “Next Steps”Here are some extensions you can try on your own:
- Add a database connection
- Deploy your Rust application
- Create a full-stack application with Rust backend and JavaScript frontend