Building APIs with Rust
API development in Rust combines performance with safety, leveraging frameworks like Actix Web or Rocket to build robust, high-throughput web services. This tutorial covers REST API development from route handling to database integration.
1. Choosing a Web Framework
Popular Options:
Actix Web: High-performance actor frameworkRocket: Developer-friendly with macrosAxum: Tokio-based from Tower ecosystem
Actix Web Setup
# Cargo.toml
[dependencies]
actix-web = "4"
serde = { version = "1.0", features = ["derive"] }
// Basic server
use actix_web::{get, App, HttpServer, Responder};
#[get("/")]
async fn hello() -> impl Responder {
"Hello from Rust API!"
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
HttpServer::new(|| {
App::new().service(hello)
})
.bind(("127.0.0.1", 8080))?
.run()
.await
}
2. RESTful Route Design
Resource-oriented endpoints with proper HTTP methods.
use actix_web::{web, HttpResponse};
// Path parameters
#[get("/users/{user_id}")]
async fn get_user(user_id: web::Path<u32>) -> HttpResponse {
HttpResponse::Ok().json(format!("User {}", user_id))
}
// Query parameters
#[get("/search")]
async fn search(query: web::Query<SearchQuery>) -> HttpResponse {
HttpResponse::Ok().json(query.into_inner())
}
// JSON body
#[post("/users")]
async fn create_user(user: web::Json<User>) -> HttpResponse {
HttpResponse::Created().json(user.into_inner())
}
// Register routes
App::new()
.service(get_user)
.service(search)
.service(create_user)
HTTP Status Codes:
200 OK- Successful GET201 Created- Successful POST400 Bad Request- Invalid input404 Not Found- Resource doesn't exist
3. Request/Response Models
Type-safe data handling with Serde serialization.
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
struct User {
id: u32,
name: String,
email: String,
}
#[derive(Deserialize)]
struct SearchQuery {
q: String,
limit: Option<u32>,
}
// Example JSON input:
// {
// "id": 1,
// "name": "Alice",
// "email": "[email protected]"
// }
Validation Tips:
- Use
validatorcrate for field validation - Implement
FromRequestfor custom parsing - Add schema documentation with
utoipaorschemars
4. Database Access
SQL and NoSQL options with connection pooling.
SQLx (PostgreSQL Example)
# Cargo.toml
[dependencies]
sqlx = { version = "0.6", features = [
"postgres",
"runtime-tokio-native-tls"
] }
use sqlx::postgres::PgPoolOptions;
struct AppState {
db: sqlx::PgPool,
}
async fn get_user(
state: web::Data<AppState>,
user_id: web::Path<i32>,
) -> Result<HttpResponse, Error> {
let user = sqlx::query_as!(
User,
"SELECT id, name, email FROM users WHERE id = $1",
user_id.into_inner()
)
.fetch_one(&state.db)
.await?;
Ok(HttpResponse::Ok().json(user))
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
let pool = PgPoolOptions::new()
.connect("postgres://user:pass@localhost/db")
.await?;
HttpServer::new(move || {
App::new()
.app_data(web::Data::new(AppState { db: pool.clone() }))
.service(web::resource("/users/{id}").to(get_user))
})
.bind(("127.0.0.1", 8080))?
.run()
.await
}
5. API Error Handling
Consistent error responses with proper status codes.
use thiserror::Error;
#[derive(Error, Debug)]
enum ApiError {
#[error("Not found")]
NotFound,
#[error("Database error")]
DatabaseError(#[from] sqlx::Error),
#[error("Validation error: {0}")]
ValidationError(String),
}
impl actix_web::error::ResponseError for ApiError {
fn error_response(&self) -> HttpResponse {
match self {
ApiError::NotFound => HttpResponse::NotFound().json("Not found"),
ApiError::DatabaseError(_) =>
HttpResponse::InternalServerError().json("Database error"),
ApiError::ValidationError(msg) =>
HttpResponse::BadRequest().json(msg),
}
}
}
async fn get_user(
state: web::Data<AppState>,
user_id: web::Path<i32>,
) -> Result<HttpResponse, ApiError> {
let user = sqlx::query_as!(
User,
"SELECT * FROM users WHERE id = $1",
user_id.into_inner()
)
.fetch_one(&state.db)
.await
.map_err(|e| match e {
sqlx::Error::RowNotFound => ApiError::NotFound,
_ => ApiError::DatabaseError(e),
})?;
Ok(HttpResponse::Ok().json(user))
}
6. Adding Middleware
Cross-cutting concerns like logging and auth.
use actix_web::middleware::Logger;
use actix_web_httpauth::middleware::HttpAuthentication;
// 1. Request logging
App::new()
.wrap(Logger::default())
// 2. JWT Authentication
let auth = HttpAuthentication::bearer(validator);
App::new()
.wrap(auth)
.service(
web::resource("/protected")
.to(protected_handler)
)
// 3. CORS
use actix_cors::Cors;
App::new()
.wrap(
Cors::default()
.allow_any_origin()
.allowed_methods(["GET", "POST"])
)
7. API Testing
Automated verification of endpoints.
#[cfg(test)]
mod tests {
use super::*;
use actix_web::test;
#[actix_web::test]
async fn test_hello() {
let app = test::init_service(
App::new().service(hello)
).await;
let req = test::TestRequest::get()
.uri("/")
.to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::OK);
let body = test::read_body(resp).await;
assert_eq!(body, "Hello from Rust API!");
}
}
×