Loading...
Loading...

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 framework
  • Rocket: Developer-friendly with macros
  • Axum: 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 GET
  • 201 Created - Successful POST
  • 400 Bad Request - Invalid input
  • 404 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": "alice@example.com"
// }

Validation Tips:

  • Use validator crate for field validation
  • Implement FromRequest for custom parsing
  • Add schema documentation with utoipa or schemars

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!");
    }
}
0 Interaction
0 Views
Views
0 Likes
×
×
×
🍪 CookieConsent@Ptutorials:~

Welcome to Ptutorials

$ Allow cookies on this site ? (y/n)

top-home