
Building Modern APIs with Rust: A Complete Setup Guide
Rust has become increasingly popular for building high-performance web services. In this guide, we'll walk through setting up a complete Rust API application using Actix-web, one of the fastest web frameworks available.
Prerequisites
- Rust installed (via rustup)
- Cargo (Rust's package manager)
- A code editor (VS Code recommended) with the rust-analyzer extension:
- Install from VS Code marketplace: search for "rust-analyzer"
- This provides real-time error checking, code completion, and inline hints
- A Supabase account (free tier works fine)
1. Project Setup
First, let's create a new Rust project:
cargo new rust-api-demo
cd rust-api-demo
Update your Cargo.toml with the necessary dependencies:
[package]
name = "rust-api-demo"
version = "0.1.0"
edition = "2021"
[dependencies]
actix-web = "4.4.0"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
tokio = { version = "1.0", features = ["full"] }
env_logger = "0.10.0"
postgrest = "1.6"
reqwest = { version = "0.11", features = ["json"] }
dotenv = "0.15"
2. Basic API Structure
Replace the contents of src/main.rs with this basic API structure:
use actix_web::{web, App, HttpResponse, HttpServer, Responder};
use serde::{Deserialize, Serialize};
use dotenv::dotenv;
use std::env;
use reqwest::Client;
#[derive(Serialize, Deserialize)]
struct Task {
id: Option<i64>,
title: String,
completed: bool,
}
struct AppState {
supabase_url: String,
supabase_key: String,
client: Client,
}
async fn get_tasks(data: web::Data<AppState>) -> impl Responder {
let client = &data.client;
let url = format!("{}/rest/v1/tasks", data.supabase_url);
let response = client
.get(&url)
.header("apikey", &data.supabase_key)
.header("Authorization", format!("Bearer {}", &data.supabase_key))
.send()
.await;
match response {
Ok(res) => {
match res.json::<Vec<Task>>().await {
Ok(tasks) => HttpResponse::Ok().json(tasks),
Err(_) => HttpResponse::InternalServerError().finish(),
}
}
Err(_) => HttpResponse::InternalServerError().finish(),
}
}
async fn create_task(
data: web::Data<AppState>,
task: web::Json<Task>
) -> impl Responder {
let client = &data.client;
let url = format!("{}/rest/v1/tasks", data.supabase_url);
let response = client
.post(&url)
.header("apikey", &data.supabase_key)
.header("Authorization", format!("Bearer {}", &data.supabase_key))
.header("Content-Type", "application/json")
.header("Prefer", "return=minimal")
.json(&task.0)
.send()
.await;
match response {
Ok(_) => HttpResponse::Created().json(task.0),
Err(_) => HttpResponse::InternalServerError().finish(),
}
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
dotenv().ok();
env_logger::init();
let supabase_url = env::var("SUPABASE_URL")
.expect("SUPABASE_URL must be set");
let supabase_key = env::var("SUPABASE_KEY")
.expect("SUPABASE_KEY must be set");
let client = Client::new();
let app_state = web::Data::new(AppState {
supabase_url,
supabase_key,
client,
});
HttpServer::new(move || {
App::new()
.app_data(app_state.clone())
.route("/tasks", web::get().to(get_tasks))
.route("/tasks", web::post().to(create_task))
})
.bind("127.0.0.1:8080")?
.run()
.await
}
3. Running the Application
Start your API server with:
cargo run
4. Testing the Endpoints
You can test the API using curl:
# Get all tasks
curl http://localhost:8080/tasks
# Create a new task
curl -X POST http://localhost:8080/tasks -H "Content-Type: application/json" -d '{"id": 3, "title": "Test API", "completed": false}'
Adding More Features
Let's add error handling and middleware:
use actix_web::{middleware, Error, Result};
// Custom error handling
#[derive(Debug, Serialize)]
struct ErrorResponse {
message: String,
}
async fn handle_task_error(err: Error) -> HttpResponse {
HttpResponse::InternalServerError().json(ErrorResponse {
message: err.to_string(),
})
}
// Add this to your App builder
App::new()
.wrap(middleware::Logger::default())
.app_data(web::JsonConfig::default().error_handler(|err, _| {
Error::from(handle_task_error(err))
}))
.route("/tasks", web::get().to(get_tasks))
.route("/tasks", web::post().to(create_task))
Best Practices
- Use proper error handling and custom error types
- Implement logging for debugging and monitoring
- Structure your code into modules for better organization
- Add input validation for your endpoints
- Use environment variables for configuration
Next Steps
To build upon this foundation, consider:
- Adding database integration (e.g., with SQLx for PostgreSQL)
- Implementing authentication and authorization
- Adding OpenAPI/Swagger documentation
- Setting up testing infrastructure
- Implementing CORS middleware for web clients
5. Integrating with Supabase
Let's add Supabase integration to store our tasks in a PostgreSQL database:
First, update your Cargo.toml with Supabase dependencies:
[dependencies]
# ... existing dependencies ...
postgrest = "1.6"
reqwest = { version = "0.11", features = ["json"] }
dotenv = "0.15"
Create a .env file in your project root:
SUPABASE_URL=your_supabase_project_url
SUPABASE_KEY=your_supabase_anon_key
Create the tasks table in your Supabase dashboard:
-- Run this in Supabase SQL editor
create table tasks (
id bigint generated by default as identity primary key,
title text not null,
completed boolean default false,
created_at timestamp with time zone default timezone('utc'::text, now())
);
-- Add row level security
alter table tasks enable row level security;
-- Allow anonymous access for demo (adjust for production)
create policy "Allow anonymous access"
on tasks for all
to anon
using (true)
with check (true);
Update your main.rs to use Supabase:
use actix_web::{web, App, HttpResponse, HttpServer, Responder};
use serde::{Deserialize, Serialize};
use dotenv::dotenv;
use std::env;
use reqwest::Client;
#[derive(Serialize, Deserialize)]
struct Task {
id: Option<i64>,
title: String,
completed: bool,
}
struct AppState {
supabase_url: String,
supabase_key: String,
client: Client,
}
async fn get_tasks(data: web::Data<AppState>) -> impl Responder {
let client = &data.client;
let url = format!("{}/rest/v1/tasks", data.supabase_url);
let response = client
.get(&url)
.header("apikey", &data.supabase_key)
.header("Authorization", format!("Bearer {}", &data.supabase_key))
.send()
.await;
match response {
Ok(res) => {
match res.json::<Vec<Task>>().await {
Ok(tasks) => HttpResponse::Ok().json(tasks),
Err(_) => HttpResponse::InternalServerError().finish(),
}
}
Err(_) => HttpResponse::InternalServerError().finish(),
}
}
async fn create_task(
data: web::Data<AppState>,
task: web::Json<Task>
) -> impl Responder {
let client = &data.client;
let url = format!("{}/rest/v1/tasks", data.supabase_url);
let response = client
.post(&url)
.header("apikey", &data.supabase_key)
.header("Authorization", format!("Bearer {}", &data.supabase_key))
.header("Content-Type", "application/json")
.header("Prefer", "return=minimal")
.json(&task.0)
.send()
.await;
match response {
Ok(_) => HttpResponse::Created().json(task.0),
Err(_) => HttpResponse::InternalServerError().finish(),
}
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
dotenv().ok();
env_logger::init();
let supabase_url = env::var("SUPABASE_URL")
.expect("SUPABASE_URL must be set");
let supabase_key = env::var("SUPABASE_KEY")
.expect("SUPABASE_KEY must be set");
let client = Client::new();
let app_state = web::Data::new(AppState {
supabase_url,
supabase_key,
client,
});
HttpServer::new(move || {
App::new()
.app_data(app_state.clone())
.route("/tasks", web::get().to(get_tasks))
.route("/tasks", web::post().to(create_task))
})
.bind("127.0.0.1:8080")?
.run()
.await
}
Now your API is connected to Supabase! The tasks will persist in your PostgreSQL database. Test it with:
# Create a task (it will be saved to Supabase)
curl -X POST http://localhost:8080/tasks -H "Content-Type: application/json" -d '{"title": "Test Supabase Integration", "completed": false}'
# Get all tasks (will fetch from Supabase)
curl http://localhost:8080/tasks
Security Considerations
- Never commit your .env file to version control
- Use appropriate Row Level Security policies in Supabase
- Consider implementing user authentication
- Use environment-specific API keys
This basic setup provides a solid foundation for building robust web services with Rust. The combination of Rust's safety guarantees and Actix-web's performance makes it an excellent choice for building high-performance APIs.