1. rust
  2. /web
  3. /rest-apis

REST API Development

Building REST APIs in Rust combines the language's performance and safety with modern web development practices. This guide covers comprehensive REST API development from design to deployment.

REST API Design Principles

RESTful Resource Design

// Example API structure for a blog application
/*
Resources and Endpoints:

Posts:
- GET    /api/posts              - List all posts
- GET    /api/posts/:id          - Get specific post
- POST   /api/posts              - Create new post
- PUT    /api/posts/:id          - Update entire post
- PATCH  /api/posts/:id          - Partial update post
- DELETE /api/posts/:id          - Delete post

Users:
- GET    /api/users              - List users
- GET    /api/users/:id          - Get user
- POST   /api/users              - Create user
- PUT    /api/users/:id          - Update user
- DELETE /api/users/:id          - Delete user

Comments:
- GET    /api/posts/:id/comments - List post comments
- POST   /api/posts/:id/comments - Create comment
- GET    /api/comments/:id       - Get comment
- PUT    /api/comments/:id       - Update comment
- DELETE /api/comments/:id       - Delete comment

Authentication:
- POST   /api/auth/login         - User login
- POST   /api/auth/logout        - User logout
- POST   /api/auth/refresh       - Refresh token
- GET    /api/auth/me            - Get current user
*/

use axum::{
    extract::{Path, Query, State},
    http::StatusCode,
    response::Json,
    routing::{get, post, put, patch, delete},
    Router,
};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use chrono::{DateTime, Utc};

// Core data models
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Post {
    pub id: Uuid,
    pub title: String,
    pub content: String,
    pub author_id: Uuid,
    pub published: bool,
    pub created_at: DateTime<Utc>,
    pub updated_at: DateTime<Utc>,
    pub tags: Vec<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct User {
    pub id: Uuid,
    pub username: String,
    pub email: String,
    pub full_name: String,
    pub avatar_url: Option<String>,
    pub created_at: DateTime<Utc>,
    pub updated_at: DateTime<Utc>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Comment {
    pub id: Uuid,
    pub post_id: Uuid,
    pub author_id: Uuid,
    pub content: String,
    pub created_at: DateTime<Utc>,
    pub updated_at: DateTime<Utc>,
}

// API Response wrappers
#[derive(Serialize)]
pub struct ApiResponse<T> {
    pub success: bool,
    pub data: Option<T>,
    pub message: Option<String>,
    pub timestamp: DateTime<Utc>,
}

#[derive(Serialize)]
pub struct PaginatedResponse<T> {
    pub data: Vec<T>,
    pub pagination: PaginationInfo,
}

#[derive(Serialize)]
pub struct PaginationInfo {
    pub page: u32,
    pub per_page: u32,
    pub total: u64,
    pub total_pages: u32,
}

impl<T> ApiResponse<T> {
    pub fn success(data: T) -> Self {
        Self {
            success: true,
            data: Some(data),
            message: None,
            timestamp: Utc::now(),
        }
    }
    
    pub fn error(message: String) -> Self {
        Self {
            success: false,
            data: None,
            message: Some(message),
            timestamp: Utc::now(),
        }
    }
}

// Request DTOs (Data Transfer Objects)
#[derive(Debug, Deserialize)]
pub struct CreatePostRequest {
    pub title: String,
    pub content: String,
    pub published: Option<bool>,
    pub tags: Option<Vec<String>>,
}

#[derive(Debug, Deserialize)]
pub struct UpdatePostRequest {
    pub title: Option<String>,
    pub content: Option<String>,
    pub published: Option<bool>,
    pub tags: Option<Vec<String>>,
}

#[derive(Debug, Deserialize)]
pub struct CreateUserRequest {
    pub username: String,
    pub email: String,
    pub full_name: String,
    pub password: String,
}

#[derive(Debug, Deserialize)]
pub struct UpdateUserRequest {
    pub username: Option<String>,
    pub email: Option<String>,
    pub full_name: Option<String>,
    pub avatar_url: Option<String>,
}

// Query parameters
#[derive(Debug, Deserialize)]
pub struct PostQuery {
    pub page: Option<u32>,
    pub per_page: Option<u32>,
    pub published: Option<bool>,
    pub author_id: Option<Uuid>,
    pub tag: Option<String>,
    pub search: Option<String>,
}

#[derive(Debug, Deserialize)]
pub struct UserQuery {
    pub page: Option<u32>,
    pub per_page: Option<u32>,
    pub search: Option<String>,
}

CRUD Operations Implementation

Complete CRUD for Posts

use std::sync::Arc;
use tokio::sync::RwLock;
use std::collections::HashMap;

// Application state and database abstraction
#[derive(Clone)]
pub struct AppState {
    pub db: Arc<Database>,
    pub auth: Arc<AuthService>,
    pub config: Arc<Config>,
}

// Simplified in-memory database for demo
#[derive(Default)]
pub struct Database {
    pub posts: RwLock<HashMap<Uuid, Post>>,
    pub users: RwLock<HashMap<Uuid, User>>,
    pub comments: RwLock<HashMap<Uuid, Comment>>,
}

// Posts CRUD handlers
pub async fn list_posts(
    Query(params): Query<PostQuery>,
    State(state): State<AppState>,
) -> Result<Json<PaginatedResponse<Post>>, ApiError> {
    let posts = state.db.posts.read().await;
    let mut filtered_posts: Vec<Post> = posts.values().cloned().collect();
    
    // Apply filters
    if let Some(published) = params.published {
        filtered_posts.retain(|p| p.published == published);
    }
    
    if let Some(author_id) = params.author_id {
        filtered_posts.retain(|p| p.author_id == author_id);
    }
    
    if let Some(tag) = &params.tag {
        filtered_posts.retain(|p| p.tags.contains(tag));
    }
    
    if let Some(search) = &params.search {
        let search_lower = search.to_lowercase();
        filtered_posts.retain(|p| {
            p.title.to_lowercase().contains(&search_lower) ||
            p.content.to_lowercase().contains(&search_lower)
        });
    }
    
    // Sort by creation date (newest first)
    filtered_posts.sort_by(|a, b| b.created_at.cmp(&a.created_at));
    
    // Pagination
    let page = params.page.unwrap_or(1);
    let per_page = params.per_page.unwrap_or(10).min(100); // Max 100 per page
    let total = filtered_posts.len() as u64;
    let total_pages = ((total as f64) / (per_page as f64)).ceil() as u32;
    
    let start = ((page - 1) * per_page) as usize;
    let end = (start + per_page as usize).min(filtered_posts.len());
    let paginated_posts = filtered_posts[start..end].to_vec();
    
    let response = PaginatedResponse {
        data: paginated_posts,
        pagination: PaginationInfo {
            page,
            per_page,
            total,
            total_pages,
        },
    };
    
    Ok(Json(response))
}

pub async fn get_post(
    Path(id): Path<Uuid>,
    State(state): State<AppState>,
) -> Result<Json<ApiResponse<Post>>, ApiError> {
    let posts = state.db.posts.read().await;
    
    match posts.get(&id) {
        Some(post) => Ok(Json(ApiResponse::success(post.clone()))),
        None => Err(ApiError::NotFound("Post not found".to_string())),
    }
}

pub async fn create_post(
    State(state): State<AppState>,
    Json(payload): Json<CreatePostRequest>,
    current_user: CurrentUser,
) -> Result<Json<ApiResponse<Post>>, ApiError> {
    // Validate request
    if payload.title.trim().is_empty() {
        return Err(ApiError::BadRequest("Title cannot be empty".to_string()));
    }
    
    if payload.content.trim().is_empty() {
        return Err(ApiError::BadRequest("Content cannot be empty".to_string()));
    }
    
    let mut posts = state.db.posts.write().await;
    let now = Utc::now();
    
    let post = Post {
        id: Uuid::new_v4(),
        title: payload.title.trim().to_string(),
        content: payload.content.trim().to_string(),
        author_id: current_user.id,
        published: payload.published.unwrap_or(false),
        created_at: now,
        updated_at: now,
        tags: payload.tags.unwrap_or_default(),
    };
    
    posts.insert(post.id, post.clone());
    
    Ok(Json(ApiResponse::success(post)))
}

pub async fn update_post(
    Path(id): Path<Uuid>,
    State(state): State<AppState>,
    Json(payload): Json<UpdatePostRequest>,
    current_user: CurrentUser,
) -> Result<Json<ApiResponse<Post>>, ApiError> {
    let mut posts = state.db.posts.write().await;
    
    let post = posts.get_mut(&id)
        .ok_or_else(|| ApiError::NotFound("Post not found".to_string()))?;
    
    // Check ownership
    if post.author_id != current_user.id && !current_user.is_admin {
        return Err(ApiError::Forbidden("You can only edit your own posts".to_string()));
    }
    
    // Update fields if provided
    if let Some(title) = payload.title {
        if title.trim().is_empty() {
            return Err(ApiError::BadRequest("Title cannot be empty".to_string()));
        }
        post.title = title.trim().to_string();
    }
    
    if let Some(content) = payload.content {
        if content.trim().is_empty() {
            return Err(ApiError::BadRequest("Content cannot be empty".to_string()));
        }
        post.content = content.trim().to_string();
    }
    
    if let Some(published) = payload.published {
        post.published = published;
    }
    
    if let Some(tags) = payload.tags {
        post.tags = tags;
    }
    
    post.updated_at = Utc::now();
    
    Ok(Json(ApiResponse::success(post.clone())))
}

pub async fn delete_post(
    Path(id): Path<Uuid>,
    State(state): State<AppState>,
    current_user: CurrentUser,
) -> Result<StatusCode, ApiError> {
    let mut posts = state.db.posts.write().await;
    
    let post = posts.get(&id)
        .ok_or_else(|| ApiError::NotFound("Post not found".to_string()))?;
    
    // Check ownership
    if post.author_id != current_user.id && !current_user.is_admin {
        return Err(ApiError::Forbidden("You can only delete your own posts".to_string()));
    }
    
    posts.remove(&id);
    
    // Also remove associated comments
    let mut comments = state.db.comments.write().await;
    comments.retain(|_, comment| comment.post_id != id);
    
    Ok(StatusCode::NO_CONTENT)
}

Comments as Nested Resources

pub async fn list_post_comments(
    Path(post_id): Path<Uuid>,
    Query(params): Query<CommentQuery>,
    State(state): State<AppState>,
) -> Result<Json<PaginatedResponse<CommentWithAuthor>>, ApiError> {
    // Verify post exists
    let posts = state.db.posts.read().await;
    if !posts.contains_key(&post_id) {
        return Err(ApiError::NotFound("Post not found".to_string()));
    }
    drop(posts);
    
    let comments = state.db.comments.read().await;
    let users = state.db.users.read().await;
    
    let mut post_comments: Vec<CommentWithAuthor> = comments
        .values()
        .filter(|c| c.post_id == post_id)
        .filter_map(|comment| {
            users.get(&comment.author_id).map(|author| CommentWithAuthor {
                id: comment.id,
                post_id: comment.post_id,
                content: comment.content.clone(),
                author: UserSummary {
                    id: author.id,
                    username: author.username.clone(),
                    full_name: author.full_name.clone(),
                    avatar_url: author.avatar_url.clone(),
                },
                created_at: comment.created_at,
                updated_at: comment.updated_at,
            })
        })
        .collect();
    
    // Sort by creation date (oldest first for comments)
    post_comments.sort_by(|a, b| a.created_at.cmp(&b.created_at));
    
    // Apply pagination
    let page = params.page.unwrap_or(1);
    let per_page = params.per_page.unwrap_or(20).min(100);
    let total = post_comments.len() as u64;
    let total_pages = ((total as f64) / (per_page as f64)).ceil() as u32;
    
    let start = ((page - 1) * per_page) as usize;
    let end = (start + per_page as usize).min(post_comments.len());
    let paginated_comments = post_comments[start..end].to_vec();
    
    let response = PaginatedResponse {
        data: paginated_comments,
        pagination: PaginationInfo {
            page,
            per_page,
            total,
            total_pages,
        },
    };
    
    Ok(Json(response))
}

pub async fn create_comment(
    Path(post_id): Path<Uuid>,
    State(state): State<AppState>,
    Json(payload): Json<CreateCommentRequest>,
    current_user: CurrentUser,
) -> Result<Json<ApiResponse<Comment>>, ApiError> {
    // Verify post exists
    let posts = state.db.posts.read().await;
    if !posts.contains_key(&post_id) {
        return Err(ApiError::NotFound("Post not found".to_string()));
    }
    drop(posts);
    
    // Validate comment content
    if payload.content.trim().is_empty() {
        return Err(ApiError::BadRequest("Comment content cannot be empty".to_string()));
    }
    
    let mut comments = state.db.comments.write().await;
    let now = Utc::now();
    
    let comment = Comment {
        id: Uuid::new_v4(),
        post_id,
        author_id: current_user.id,
        content: payload.content.trim().to_string(),
        created_at: now,
        updated_at: now,
    };
    
    comments.insert(comment.id, comment.clone());
    
    Ok(Json(ApiResponse::success(comment)))
}

#[derive(Debug, Serialize)]
pub struct CommentWithAuthor {
    pub id: Uuid,
    pub post_id: Uuid,
    pub content: String,
    pub author: UserSummary,
    pub created_at: DateTime<Utc>,
    pub updated_at: DateTime<Utc>,
}

#[derive(Debug, Serialize)]
pub struct UserSummary {
    pub id: Uuid,
    pub username: String,
    pub full_name: String,
    pub avatar_url: Option<String>,
}

#[derive(Debug, Deserialize)]
pub struct CreateCommentRequest {
    pub content: String,
}

#[derive(Debug, Deserialize)]
pub struct CommentQuery {
    pub page: Option<u32>,
    pub per_page: Option<u32>,
}

Authentication & Authorization

JWT-based Authentication

use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation, Algorithm};
use axum::{
    async_trait,
    extract::{FromRequestParts, TypedHeader},
    headers::{authorization::Bearer, Authorization},
    http::request::Parts,
    RequestPartsExt,
};
use serde::{Deserialize, Serialize};
use std::time::{SystemTime, UNIX_EPOCH};

#[derive(Debug, Serialize, Deserialize)]
pub struct Claims {
    pub sub: String,        // Subject (user ID)
    pub username: String,   // Username
    pub email: String,      // Email
    pub is_admin: bool,     // Admin flag
    pub exp: u64,          // Expiration time
    pub iat: u64,          // Issued at
    pub jti: String,       // JWT ID
}

#[derive(Clone)]
pub struct AuthService {
    encoding_key: EncodingKey,
    decoding_key: DecodingKey,
    validation: Validation,
}

impl AuthService {
    pub fn new(secret: &str) -> Self {
        let mut validation = Validation::new(Algorithm::HS256);
        validation.required_spec_claims.insert("exp".to_string());
        validation.required_spec_claims.insert("sub".to_string());
        
        Self {
            encoding_key: EncodingKey::from_secret(secret.as_ref()),
            decoding_key: DecodingKey::from_secret(secret.as_ref()),
            validation,
        }
    }
    
    pub fn create_token(&self, user: &User) -> Result<String, AuthError> {
        let now = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .unwrap()
            .as_secs();
        
        let claims = Claims {
            sub: user.id.to_string(),
            username: user.username.clone(),
            email: user.email.clone(),
            is_admin: false, // This should come from user data
            exp: now + 24 * 60 * 60, // 24 hours
            iat: now,
            jti: Uuid::new_v4().to_string(),
        };
        
        encode(&Header::default(), &claims, &self.encoding_key)
            .map_err(|_| AuthError::TokenCreation)
    }
    
    pub fn verify_token(&self, token: &str) -> Result<Claims, AuthError> {
        decode::<Claims>(token, &self.decoding_key, &self.validation)
            .map(|data| data.claims)
            .map_err(|_| AuthError::InvalidToken)
    }
}

// Current user extractor
#[derive(Debug, Clone)]
pub struct CurrentUser {
    pub id: Uuid,
    pub username: String,
    pub email: String,
    pub is_admin: bool,
}

#[async_trait]
impl<S> FromRequestParts<S> for CurrentUser
where
    S: Send + Sync,
{
    type Rejection = AuthError;
    
    async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
        // Extract authorization header
        let TypedHeader(Authorization(bearer)) = parts
            .extract::<TypedHeader<Authorization<Bearer>>>()
            .await
            .map_err(|_| AuthError::MissingToken)?;
        
        // Get auth service from app state
        let auth_service = parts
            .extensions
            .get::<Arc<AuthService>>()
            .ok_or(AuthError::ConfigError)?;
        
        // Verify token
        let claims = auth_service.verify_token(bearer.token())?;
        
        // Parse user ID
        let user_id = Uuid::parse_str(&claims.sub)
            .map_err(|_| AuthError::InvalidToken)?;
        
        Ok(CurrentUser {
            id: user_id,
            username: claims.username,
            email: claims.email,
            is_admin: claims.is_admin,
        })
    }
}

// Optional user extractor (for endpoints that work with or without auth)
#[derive(Debug, Clone)]
pub struct OptionalUser(pub Option<CurrentUser>);

#[async_trait]
impl<S> FromRequestParts<S> for OptionalUser
where
    S: Send + Sync,
{
    type Rejection = std::convert::Infallible;
    
    async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
        match CurrentUser::from_request_parts(parts, state).await {
            Ok(user) => Ok(OptionalUser(Some(user))),
            Err(_) => Ok(OptionalUser(None)),
        }
    }
}

// Authentication endpoints
pub async fn login(
    State(state): State<AppState>,
    Json(credentials): Json<LoginRequest>,
) -> Result<Json<LoginResponse>, ApiError> {
    let users = state.db.users.read().await;
    
    // Find user by username or email
    let user = users
        .values()
        .find(|u| u.username == credentials.username || u.email == credentials.username)
        .ok_or_else(|| ApiError::Unauthorized("Invalid credentials".to_string()))?;
    
    // Verify password (in real app, use proper password hashing)
    if !verify_password(&credentials.password, &user.password_hash) {
        return Err(ApiError::Unauthorized("Invalid credentials".to_string()));
    }
    
    // Create JWT token
    let token = state.auth.create_token(user)
        .map_err(|_| ApiError::InternalError("Failed to create token".to_string()))?;
    
    Ok(Json(LoginResponse {
        token,
        user: UserProfile {
            id: user.id,
            username: user.username.clone(),
            email: user.email.clone(),
            full_name: user.full_name.clone(),
            avatar_url: user.avatar_url.clone(),
            created_at: user.created_at,
        },
    }))
}

pub async fn register(
    State(state): State<AppState>,
    Json(payload): Json<CreateUserRequest>,
) -> Result<Json<ApiResponse<UserProfile>>, ApiError> {
    // Validate input
    if payload.username.trim().is_empty() {
        return Err(ApiError::BadRequest("Username cannot be empty".to_string()));
    }
    
    if payload.email.trim().is_empty() || !is_valid_email(&payload.email) {
        return Err(ApiError::BadRequest("Invalid email address".to_string()));
    }
    
    if payload.password.len() < 8 {
        return Err(ApiError::BadRequest("Password must be at least 8 characters".to_string()));
    }
    
    let mut users = state.db.users.write().await;
    
    // Check if username or email already exists
    for user in users.values() {
        if user.username == payload.username {
            return Err(ApiError::Conflict("Username already exists".to_string()));
        }
        if user.email == payload.email {
            return Err(ApiError::Conflict("Email already exists".to_string()));
        }
    }
    
    // Create new user
    let now = Utc::now();
    let password_hash = hash_password(&payload.password)?;
    
    let user = User {
        id: Uuid::new_v4(),
        username: payload.username.trim().to_string(),
        email: payload.email.trim().to_lowercase(),
        full_name: payload.full_name.trim().to_string(),
        password_hash,
        avatar_url: None,
        created_at: now,
        updated_at: now,
    };
    
    let user_profile = UserProfile {
        id: user.id,
        username: user.username.clone(),
        email: user.email.clone(),
        full_name: user.full_name.clone(),
        avatar_url: user.avatar_url.clone(),
        created_at: user.created_at,
    };
    
    users.insert(user.id, user);
    
    Ok(Json(ApiResponse::success(user_profile)))
}

pub async fn get_current_user(
    current_user: CurrentUser,
) -> Result<Json<ApiResponse<CurrentUser>>, ApiError> {
    Ok(Json(ApiResponse::success(current_user)))
}

#[derive(Debug, Deserialize)]
pub struct LoginRequest {
    pub username: String, // Can be username or email
    pub password: String,
}

#[derive(Debug, Serialize)]
pub struct LoginResponse {
    pub token: String,
    pub user: UserProfile,
}

#[derive(Debug, Serialize)]
pub struct UserProfile {
    pub id: Uuid,
    pub username: String,
    pub email: String,
    pub full_name: String,
    pub avatar_url: Option<String>,
    pub created_at: DateTime<Utc>,
}

// Error types
#[derive(Debug, thiserror::Error)]
pub enum AuthError {
    #[error("Missing authentication token")]
    MissingToken,
    #[error("Invalid authentication token")]
    InvalidToken,
    #[error("Token creation failed")]
    TokenCreation,
    #[error("Authentication configuration error")]
    ConfigError,
}

// Helper functions (simplified for demo)
fn verify_password(password: &str, hash: &str) -> bool {
    // In real app, use bcrypt or argon2
    password == hash // NEVER do this in production!
}

fn hash_password(password: &str) -> Result<String, ApiError> {
    // In real app, use bcrypt or argon2
    Ok(password.to_string()) // NEVER do this in production!
}

fn is_valid_email(email: &str) -> bool {
    email.contains('@') // Very basic validation
}

Error Handling

Comprehensive Error System

use axum::{
    http::StatusCode,
    response::{IntoResponse, Response},
    Json,
};
use thiserror::Error;

#[derive(Debug, Error)]
pub enum ApiError {
    #[error("Bad request: {0}")]
    BadRequest(String),
    
    #[error("Unauthorized: {0}")]
    Unauthorized(String),
    
    #[error("Forbidden: {0}")]
    Forbidden(String),
    
    #[error("Not found: {0}")]
    NotFound(String),
    
    #[error("Conflict: {0}")]
    Conflict(String),
    
    #[error("Validation failed: {0}")]
    ValidationError(String),
    
    #[error("Internal server error: {0}")]
    InternalError(String),
    
    #[error("Service unavailable: {0}")]
    ServiceUnavailable(String),
    
    #[error("Database error: {0}")]
    DatabaseError(String),
    
    #[error("Authentication error: {0}")]
    AuthError(#[from] AuthError),
}

impl IntoResponse for ApiError {
    fn into_response(self) -> Response {
        let (status_code, error_code, message) = match self {
            ApiError::BadRequest(msg) => (StatusCode::BAD_REQUEST, "BAD_REQUEST", msg),
            ApiError::Unauthorized(msg) => (StatusCode::UNAUTHORIZED, "UNAUTHORIZED", msg),
            ApiError::Forbidden(msg) => (StatusCode::FORBIDDEN, "FORBIDDEN", msg),
            ApiError::NotFound(msg) => (StatusCode::NOT_FOUND, "NOT_FOUND", msg),
            ApiError::Conflict(msg) => (StatusCode::CONFLICT, "CONFLICT", msg),
            ApiError::ValidationError(msg) => (StatusCode::UNPROCESSABLE_ENTITY, "VALIDATION_ERROR", msg),
            ApiError::InternalError(msg) => (StatusCode::INTERNAL_SERVER_ERROR, "INTERNAL_ERROR", msg),
            ApiError::ServiceUnavailable(msg) => (StatusCode::SERVICE_UNAVAILABLE, "SERVICE_UNAVAILABLE", msg),
            ApiError::DatabaseError(msg) => (StatusCode::INTERNAL_SERVER_ERROR, "DATABASE_ERROR", msg),
            ApiError::AuthError(auth_err) => {
                match auth_err {
                    AuthError::MissingToken | AuthError::InvalidToken => {
                        (StatusCode::UNAUTHORIZED, "INVALID_TOKEN", auth_err.to_string())
                    }
                    _ => (StatusCode::INTERNAL_SERVER_ERROR, "AUTH_ERROR", auth_err.to_string())
                }
            }
        };
        
        let error_response = ErrorResponse {
            success: false,
            error: ErrorDetail {
                code: error_code.to_string(),
                message,
                timestamp: Utc::now(),
            },
        };
        
        (status_code, Json(error_response)).into_response()
    }
}

#[derive(Serialize)]
struct ErrorResponse {
    success: bool,
    error: ErrorDetail,
}

#[derive(Serialize)]
struct ErrorDetail {
    code: String,
    message: String,
    timestamp: DateTime<Utc>,
}

// Result type alias for convenience
pub type ApiResult<T> = Result<T, ApiError>;

// Validation error handling
pub fn validate_post_request(req: &CreatePostRequest) -> Result<(), ApiError> {
    let mut errors = Vec::new();
    
    if req.title.trim().is_empty() {
        errors.push("Title cannot be empty");
    } else if req.title.len() > 200 {
        errors.push("Title cannot exceed 200 characters");
    }
    
    if req.content.trim().is_empty() {
        errors.push("Content cannot be empty");
    } else if req.content.len() > 50000 {
        errors.push("Content cannot exceed 50,000 characters");
    }
    
    if let Some(tags) = &req.tags {
        if tags.len() > 10 {
            errors.push("Cannot have more than 10 tags");
        }
        
        for tag in tags {
            if tag.trim().is_empty() {
                errors.push("Tags cannot be empty");
                break;
            }
            if tag.len() > 50 {
                errors.push("Tags cannot exceed 50 characters");
                break;
            }
        }
    }
    
    if !errors.is_empty() {
        return Err(ApiError::ValidationError(errors.join(", ")));
    }
    
    Ok(())
}

// Middleware for error logging
pub async fn error_handler(
    uri: axum::http::Uri,
    method: axum::http::Method,
    error: ApiError,
) -> Response {
    // Log error details
    match &error {
        ApiError::InternalError(_) | ApiError::DatabaseError(_) => {
            tracing::error!(
                method = %method,
                uri = %uri,
                error = %error,
                "Internal server error occurred"
            );
        }
        _ => {
            tracing::warn!(
                method = %method,
                uri = %uri,
                error = %error,
                "Client error occurred"
            );
        }
    }
    
    error.into_response()
}

Input Validation

Request Validation with Validator

use validator::{Validate, ValidationError, ValidationErrors};
use axum::{
    async_trait,
    extract::{FromRequest, Request},
    Json,
};

// Validated JSON extractor
pub struct ValidatedJson<T>(pub T);

#[async_trait]
impl<T, S> FromRequest<S> for ValidatedJson<T>
where
    T: DeserializeOwned + Validate,
    S: Send + Sync,
    Json<T>: FromRequest<S>,
{
    type Rejection = ApiError;

    async fn from_request(req: Request, state: &S) -> Result<Self, Self::Rejection> {
        let Json(value) = Json::<T>::from_request(req, state)
            .await
            .map_err(|err| ApiError::BadRequest(format!("Invalid JSON: {}", err)))?;
        
        value.validate()
            .map_err(|errors| ApiError::ValidationError(format_validation_errors(errors)))?;
        
        Ok(ValidatedJson(value))
    }
}

// Enhanced request DTOs with validation
#[derive(Debug, Deserialize, Validate)]
pub struct CreatePostRequestValidated {
    #[validate(length(min = 1, max = 200, message = "Title must be between 1 and 200 characters"))]
    pub title: String,
    
    #[validate(length(min = 1, max = 50000, message = "Content must be between 1 and 50,000 characters"))]
    pub content: String,
    
    pub published: Option<bool>,
    
    #[validate(length(max = 10, message = "Cannot have more than 10 tags"))]
    #[validate(custom = "validate_tags")]
    pub tags: Option<Vec<String>>,
}

#[derive(Debug, Deserialize, Validate)]
pub struct CreateUserRequestValidated {
    #[validate(length(min = 3, max = 30, message = "Username must be between 3 and 30 characters"))]
    #[validate(regex = "USERNAME_REGEX", message = "Username can only contain letters, numbers, and underscores"))]
    pub username: String,
    
    #[validate(email(message = "Invalid email address"))]
    pub email: String,
    
    #[validate(length(min = 1, max = 100, message = "Full name must be between 1 and 100 characters"))]
    pub full_name: String,
    
    #[validate(length(min = 8, message = "Password must be at least 8 characters"))]
    #[validate(custom = "validate_password")]
    pub password: String,
}

// Custom validators
fn validate_tags(tags: &[String]) -> Result<(), ValidationError> {
    for tag in tags {
        if tag.trim().is_empty() {
            return Err(ValidationError::new("Tags cannot be empty"));
        }
        if tag.len() > 50 {
            return Err(ValidationError::new("Tags cannot exceed 50 characters"));
        }
        if !tag.chars().all(|c| c.is_alphanumeric() || c == '-' || c == '_') {
            return Err(ValidationError::new("Tags can only contain letters, numbers, hyphens, and underscores"));
        }
    }
    Ok(())
}

fn validate_password(password: &str) -> Result<(), ValidationError> {
    let has_lowercase = password.chars().any(|c| c.is_lowercase());
    let has_uppercase = password.chars().any(|c| c.is_uppercase());
    let has_digit = password.chars().any(|c| c.is_numeric());
    let has_special = password.chars().any(|c| !c.is_alphanumeric());
    
    if !has_lowercase {
        return Err(ValidationError::new("Password must contain at least one lowercase letter"));
    }
    if !has_uppercase {
        return Err(ValidationError::new("Password must contain at least one uppercase letter"));
    }
    if !has_digit {
        return Err(ValidationError::new("Password must contain at least one digit"));
    }
    if !has_special {
        return Err(ValidationError::new("Password must contain at least one special character"));
    }
    
    Ok(())
}

// Regex for username validation
lazy_static::lazy_static! {
    static ref USERNAME_REGEX: regex::Regex = regex::Regex::new(r"^[a-zA-Z0-9_]+$").unwrap();
}

// Format validation errors into user-friendly messages
fn format_validation_errors(errors: ValidationErrors) -> String {
    let mut messages = Vec::new();
    
    for (field, field_errors) in errors.field_errors() {
        for error in field_errors {
            let message = error.message
                .as_ref()
                .map(|m| m.to_string())
                .unwrap_or_else(|| format!("Invalid value for field '{}'", field));
            messages.push(message);
        }
    }
    
    messages.join(", ")
}

// Updated handler using validated input
pub async fn create_post_validated(
    State(state): State<AppState>,
    ValidatedJson(payload): ValidatedJson<CreatePostRequestValidated>,
    current_user: CurrentUser,
) -> Result<Json<ApiResponse<Post>>, ApiError> {
    let mut posts = state.db.posts.write().await;
    let now = Utc::now();
    
    let post = Post {
        id: Uuid::new_v4(),
        title: payload.title.trim().to_string(),
        content: payload.content.trim().to_string(),
        author_id: current_user.id,
        published: payload.published.unwrap_or(false),
        created_at: now,
        updated_at: now,
        tags: payload.tags.unwrap_or_default(),
    };
    
    posts.insert(post.id, post.clone());
    
    Ok(Json(ApiResponse::success(post)))
}

API Testing

Unit and Integration Tests

#[cfg(test)]
mod tests {
    use super::*;
    use axum::{
        body::Body,
        http::{Request, StatusCode},
    };
    use tower::ServiceExt; // for `oneshot`
    use serde_json::json;

    // Test setup helpers
    async fn create_test_app() -> Router {
        let db = Arc::new(Database::default());
        let auth = Arc::new(AuthService::new("test-secret"));
        let config = Arc::new(Config::default());
        
        let state = AppState { db, auth, config };
        
        create_router(state)
    }
    
    async fn create_test_user(app_state: &AppState) -> (Uuid, String) {
        let user = User {
            id: Uuid::new_v4(),
            username: "testuser".to_string(),
            email: "[email protected]".to_string(),
            full_name: "Test User".to_string(),
            password_hash: "password".to_string(), // Not secure, just for testing
            avatar_url: None,
            created_at: Utc::now(),
            updated_at: Utc::now(),
        };
        
        let token = app_state.auth.create_token(&user).unwrap();
        let user_id = user.id;
        
        app_state.db.users.write().await.insert(user.id, user);
        
        (user_id, token)
    }

    #[tokio::test]
    async fn test_create_post() {
        let app = create_test_app().await;
        let app_state = app.layer_stack().get::<AppState>().unwrap();
        let (_user_id, token) = create_test_user(app_state).await;
        
        let post_data = json!({
            "title": "Test Post",
            "content": "This is a test post content",
            "published": true,
            "tags": ["rust", "web"]
        });
        
        let response = app
            .oneshot(
                Request::builder()
                    .method("POST")
                    .uri("/api/posts")
                    .header("authorization", format!("Bearer {}", token))
                    .header("content-type", "application/json")
                    .body(Body::from(post_data.to_string()))
                    .unwrap(),
            )
            .await
            .unwrap();
        
        assert_eq!(response.status(), StatusCode::OK);
        
        let body_bytes = axum::body::to_bytes(response.into_body(), usize::MAX).await.unwrap();
        let response_data: serde_json::Value = serde_json::from_slice(&body_bytes).unwrap();
        
        assert_eq!(response_data["success"], true);
        assert_eq!(response_data["data"]["title"], "Test Post");
        assert_eq!(response_data["data"]["published"], true);
    }

    #[tokio::test]
    async fn test_get_posts_pagination() {
        let app = create_test_app().await;
        let app_state = app.layer_stack().get::<AppState>().unwrap();
        let (user_id, _token) = create_test_user(app_state).await;
        
        // Create test posts
        let mut posts = app_state.db.posts.write().await;
        for i in 0..25 {
            let post = Post {
                id: Uuid::new_v4(),
                title: format!("Test Post {}", i),
                content: format!("Content for post {}", i),
                author_id: user_id,
                published: true,
                created_at: Utc::now(),
                updated_at: Utc::now(),
                tags: vec![],
            };
            posts.insert(post.id, post);
        }
        drop(posts);
        
        // Test pagination
        let response = app
            .oneshot(
                Request::builder()
                    .method("GET")
                    .uri("/api/posts?page=2&per_page=10")
                    .body(Body::empty())
                    .unwrap(),
            )
            .await
            .unwrap();
        
        assert_eq!(response.status(), StatusCode::OK);
        
        let body_bytes = axum::body::to_bytes(response.into_body(), usize::MAX).await.unwrap();
        let response_data: serde_json::Value = serde_json::from_slice(&body_bytes).unwrap();
        
        assert_eq!(response_data["data"].as_array().unwrap().len(), 10);
        assert_eq!(response_data["pagination"]["page"], 2);
        assert_eq!(response_data["pagination"]["per_page"], 10);
        assert_eq!(response_data["pagination"]["total"], 25);
        assert_eq!(response_data["pagination"]["total_pages"], 3);
    }

    #[tokio::test]
    async fn test_authentication_required() {
        let app = create_test_app().await;
        
        let post_data = json!({
            "title": "Test Post",
            "content": "This should fail without auth"
        });
        
        let response = app
            .oneshot(
                Request::builder()
                    .method("POST")
                    .uri("/api/posts")
                    .header("content-type", "application/json")
                    .body(Body::from(post_data.to_string()))
                    .unwrap(),
            )
            .await
            .unwrap();
        
        assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
    }

    #[tokio::test]
    async fn test_validation_errors() {
        let app = create_test_app().await;
        let app_state = app.layer_stack().get::<AppState>().unwrap();
        let (_user_id, token) = create_test_user(app_state).await;
        
        // Test with empty title
        let invalid_post = json!({
            "title": "",
            "content": "This has an empty title"
        });
        
        let response = app
            .oneshot(
                Request::builder()
                    .method("POST")
                    .uri("/api/posts")
                    .header("authorization", format!("Bearer {}", token))
                    .header("content-type", "application/json")
                    .body(Body::from(invalid_post.to_string()))
                    .unwrap(),
            )
            .await
            .unwrap();
        
        assert_eq!(response.status(), StatusCode::UNPROCESSABLE_ENTITY);
        
        let body_bytes = axum::body::to_bytes(response.into_body(), usize::MAX).await.unwrap();
        let response_data: serde_json::Value = serde_json::from_slice(&body_bytes).unwrap();
        
        assert_eq!(response_data["success"], false);
        assert!(response_data["error"]["message"].as_str().unwrap().contains("Title"));
    }

    #[tokio::test]
    async fn test_user_registration() {
        let app = create_test_app().await;
        
        let user_data = json!({
            "username": "newuser",
            "email": "[email protected]",
            "full_name": "New User",
            "password": "SecurePass123!"
        });
        
        let response = app
            .oneshot(
                Request::builder()
                    .method("POST")
                    .uri("/api/auth/register")
                    .header("content-type", "application/json")
                    .body(Body::from(user_data.to_string()))
                    .unwrap(),
            )
            .await
            .unwrap();
        
        assert_eq!(response.status(), StatusCode::OK);
        
        let body_bytes = axum::body::to_bytes(response.into_body(), usize::MAX).await.unwrap();
        let response_data: serde_json::Value = serde_json::from_slice(&body_bytes).unwrap();
        
        assert_eq!(response_data["success"], true);
        assert_eq!(response_data["data"]["username"], "newuser");
        assert_eq!(response_data["data"]["email"], "[email protected]");
    }

    #[tokio::test]
    async fn test_duplicate_username_registration() {
        let app = create_test_app().await;
        let app_state = app.layer_stack().get::<AppState>().unwrap();
        let (_user_id, _token) = create_test_user(app_state).await;
        
        // Try to register with existing username
        let duplicate_user = json!({
            "username": "testuser", // This already exists
            "email": "[email protected]",
            "full_name": "Different User",
            "password": "SecurePass123!"
        });
        
        let response = app
            .oneshot(
                Request::builder()
                    .method("POST")
                    .uri("/api/auth/register")
                    .header("content-type", "application/json")
                    .body(Body::from(duplicate_user.to_string()))
                    .unwrap(),
            )
            .await
            .unwrap();
        
        assert_eq!(response.status(), StatusCode::CONFLICT);
        
        let body_bytes = axum::body::to_bytes(response.into_body(), usize::MAX).await.unwrap();
        let response_data: serde_json::Value = serde_json::from_slice(&body_bytes).unwrap();
        
        assert_eq!(response_data["success"], false);
        assert!(response_data["error"]["message"].as_str().unwrap().contains("Username already exists"));
    }

    // Performance test example
    #[tokio::test]
    async fn test_concurrent_post_creation() {
        let app = create_test_app().await;
        let app_state = app.layer_stack().get::<AppState>().unwrap();
        let (_user_id, token) = create_test_user(app_state).await;
        
        let tasks: Vec<_> = (0..100)
            .map(|i| {
                let app_clone = app.clone();
                let token_clone = token.clone();
                
                tokio::spawn(async move {
                    let post_data = json!({
                        "title": format!("Concurrent Post {}", i),
                        "content": format!("Content for concurrent post {}", i),
                        "published": true
                    });
                    
                    app_clone
                        .oneshot(
                            Request::builder()
                                .method("POST")
                                .uri("/api/posts")
                                .header("authorization", format!("Bearer {}", token_clone))
                                .header("content-type", "application/json")
                                .body(Body::from(post_data.to_string()))
                                .unwrap(),
                        )
                        .await
                        .unwrap()
                        .status()
                })
            })
            .collect();
        
        let results = futures::future::join_all(tasks).await;
        
        // All requests should succeed
        for result in results {
            assert_eq!(result.unwrap(), StatusCode::OK);
        }
        
        // Verify all posts were created
        let posts = app_state.db.posts.read().await;
        assert_eq!(posts.len(), 100);
    }
}

// Load testing with criterion
#[cfg(test)]
mod bench {
    use super::*;
    use criterion::{criterion_group, criterion_main, Criterion};
    
    fn bench_post_creation(c: &mut Criterion) {
        let rt = tokio::runtime::Runtime::new().unwrap();
        
        c.bench_function("create_post", |b| {
            b.to_async(&rt).iter(|| async {
                let app = create_test_app().await;
                let app_state = app.layer_stack().get::<AppState>().unwrap();
                let (_user_id, token) = create_test_user(app_state).await;
                
                let post_data = json!({
                    "title": "Benchmark Post",
                    "content": "Benchmark content",
                    "published": true
                });
                
                app.oneshot(
                    Request::builder()
                        .method("POST")
                        .uri("/api/posts")
                        .header("authorization", format!("Bearer {}", token))
                        .header("content-type", "application/json")
                        .body(Body::from(post_data.to_string()))
                        .unwrap(),
                )
                .await
                .unwrap()
            });
        });
    }
    
    criterion_group!(benches, bench_post_creation);
    criterion_main!(benches);
}

API Documentation

OpenAPI/Swagger Documentation

use utoipa::{OpenApi, ToSchema};
use utoipa_swagger_ui::SwaggerUi;

#[derive(OpenApi)]
#[openapi(
    paths(
        list_posts,
        get_post,
        create_post,
        update_post,
        delete_post,
        login,
        register,
        get_current_user
    ),
    components(
        schemas(
            Post,
            User,
            CreatePostRequest,
            UpdatePostRequest,
            LoginRequest,
            LoginResponse,
            ApiResponse<Post>,
            PaginatedResponse<Post>,
            ErrorResponse
        )
    ),
    tags(
        (name = "posts", description = "Blog post management"),
        (name = "auth", description = "Authentication endpoints"),
        (name = "users", description = "User management")
    ),
    info(
        title = "Blog API",
        description = "A REST API for a blog application built with Rust and Axum",
        version = "1.0.0",
        contact(
            name = "API Support",
            email = "[email protected]"
        ),
        license(
            name = "MIT",
            url = "https://opensource.org/licenses/MIT"
        )
    ),
    servers(
        (url = "http://localhost:3000", description = "Local development server"),
        (url = "https://api.blogapi.com", description = "Production server")
    ),
    security(
        ("bearer_auth" = [])
    )
)]
pub struct ApiDoc;

// Add schemas for documentation
#[derive(ToSchema, Serialize, Deserialize)]
pub struct Post {
    #[schema(example = "01234567-89ab-cdef-0123-456789abcdef")]
    pub id: Uuid,
    #[schema(example = "My First Blog Post")]
    pub title: String,
    #[schema(example = "This is the content of my first blog post...")]
    pub content: String,
    #[schema(example = "01234567-89ab-cdef-0123-456789abcdef")]
    pub author_id: Uuid,
    #[schema(example = true)]
    pub published: bool,
    #[schema(example = "2023-01-01T00:00:00Z")]
    pub created_at: DateTime<Utc>,
    #[schema(example = "2023-01-01T00:00:00Z")]
    pub updated_at: DateTime<Utc>,
    #[schema(example = json!(["rust", "web", "api"]))]
    pub tags: Vec<String>,
}

// Document API endpoints
#[utoipa::path(
    get,
    path = "/api/posts",
    tag = "posts",
    summary = "List blog posts",
    description = "Retrieve a paginated list of blog posts with optional filtering",
    params(
        ("page" = Option<u32>, Query, description = "Page number (default: 1)"),
        ("per_page" = Option<u32>, Query, description = "Items per page (default: 10, max: 100)"),
        ("published" = Option<bool>, Query, description = "Filter by published status"),
        ("author_id" = Option<Uuid>, Query, description = "Filter by author ID"),
        ("tag" = Option<String>, Query, description = "Filter by tag"),
        ("search" = Option<String>, Query, description = "Search in title and content")
    ),
    responses(
        (status = 200, description = "List of posts retrieved successfully", body = PaginatedResponse<Post>),
        (status = 400, description = "Invalid query parameters", body = ErrorResponse)
    )
)]
pub async fn list_posts(
    Query(params): Query<PostQuery>,
    State(state): State<AppState>,
) -> Result<Json<PaginatedResponse<Post>>, ApiError> {
    // Implementation here...
}

#[utoipa::path(
    post,
    path = "/api/posts",
    tag = "posts",
    summary = "Create a new blog post",
    description = "Create a new blog post. Requires authentication.",
    request_body = CreatePostRequest,
    responses(
        (status = 200, description = "Post created successfully", body = ApiResponse<Post>),
        (status = 400, description = "Invalid input", body = ErrorResponse),
        (status = 401, description = "Authentication required", body = ErrorResponse),
        (status = 422, description = "Validation failed", body = ErrorResponse)
    ),
    security(
        ("bearer_auth" = [])
    )
)]
pub async fn create_post(
    State(state): State<AppState>,
    Json(payload): Json<CreatePostRequest>,
    current_user: CurrentUser,
) -> Result<Json<ApiResponse<Post>>, ApiError> {
    // Implementation here...
}

// Add security scheme
impl ApiDoc {
    pub fn with_security() -> utoipa::openapi::OpenApi {
        use utoipa::openapi::security::{HttpAuthScheme, HttpBuilder, SecurityScheme};
        
        Self::openapi()
            .components(Some(
                utoipa::openapi::ComponentsBuilder::new()
                    .security_scheme(
                        "bearer_auth",
                        SecurityScheme::Http(
                            HttpBuilder::new()
                                .scheme(HttpAuthScheme::Bearer)
                                .bearer_format("JWT")
                                .build(),
                        ),
                    )
                    .build(),
            ))
    }
}

// Router setup with Swagger UI
pub fn create_router(state: AppState) -> Router {
    Router::new()
        .merge(SwaggerUi::new("/swagger-ui").url("/api-docs/openapi.json", ApiDoc::with_security()))
        .route("/api/posts", get(list_posts).post(create_post))
        .route("/api/posts/:id", get(get_post).put(update_post).delete(delete_post))
        .route("/api/auth/login", post(login))
        .route("/api/auth/register", post(register))
        .route("/api/auth/me", get(get_current_user))
        .with_state(state)
}

Performance and Deployment

Production Configuration

// Configuration management
#[derive(Debug, Clone, serde::Deserialize)]
pub struct Config {
    pub server: ServerConfig,
    pub database: DatabaseConfig,
    pub auth: AuthConfig,
    pub logging: LoggingConfig,
    pub cors: CorsConfig,
}

#[derive(Debug, Clone, serde::Deserialize)]
pub struct ServerConfig {
    pub host: String,
    pub port: u16,
    pub workers: Option<usize>,
    pub max_connections: usize,
    pub timeout_seconds: u64,
}

#[derive(Debug, Clone, serde::Deserialize)]
pub struct DatabaseConfig {
    pub url: String,
    pub max_connections: u32,
    pub min_connections: u32,
    pub connect_timeout_seconds: u64,
    pub idle_timeout_seconds: u64,
}

#[derive(Debug, Clone, serde::Deserialize)]
pub struct AuthConfig {
    pub jwt_secret: String,
    pub token_expiry_hours: u64,
    pub refresh_token_expiry_days: u64,
}

#[derive(Debug, Clone, serde::Deserialize)]
pub struct LoggingConfig {
    pub level: String,
    pub json_format: bool,
}

#[derive(Debug, Clone, serde::Deserialize)]
pub struct CorsConfig {
    pub allowed_origins: Vec<String>,
    pub allowed_methods: Vec<String>,
    pub allowed_headers: Vec<String>,
    pub max_age_seconds: usize,
}

impl Config {
    pub fn from_env() -> Result<Self, config::ConfigError> {
        let settings = config::Config::builder()
            .add_source(config::File::with_name("config/default"))
            .add_source(config::File::with_name("config/production").required(false))
            .add_source(config::Environment::with_prefix("API"))
            .build()?;
        
        settings.try_deserialize()
    }
}

// Production server setup
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Load configuration
    let config = Config::from_env()?;
    
    // Setup logging
    setup_logging(&config.logging)?;
    
    // Setup database
    let db_pool = create_database_pool(&config.database).await?;
    
    // Setup authentication
    let auth_service = Arc::new(AuthService::new(&config.auth.jwt_secret));
    
    // Create application state
    let app_state = AppState {
        db: Arc::new(Database::new(db_pool)),
        auth: auth_service,
        config: Arc::new(config.clone()),
    };
    
    // Setup CORS
    let cors = setup_cors(&config.cors);
    
    // Create router
    let app = create_router(app_state)
        .layer(cors)
        .layer(TraceLayer::new_for_http())
        .layer(CompressionLayer::new())
        .layer(TimeoutLayer::new(Duration::from_secs(config.server.timeout_seconds)));
    
    // Start server
    let bind_address = format!("{}:{}", config.server.host, config.server.port);
    let listener = tokio::net::TcpListener::bind(&bind_address).await?;
    
    tracing::info!("Server starting on {}", bind_address);
    
    axum::serve(listener, app)
        .with_graceful_shutdown(shutdown_signal())
        .await?;
    
    Ok(())
}

fn setup_logging(config: &LoggingConfig) -> Result<(), Box<dyn std::error::Error>> {
    use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
    
    let env_filter = tracing_subscriber::EnvFilter::try_from_default_env()
        .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new(&config.level));
    
    if config.json_format {
        tracing_subscriber::registry()
            .with(env_filter)
            .with(tracing_subscriber::fmt::layer().json())
            .init();
    } else {
        tracing_subscriber::registry()
            .with(env_filter)
            .with(tracing_subscriber::fmt::layer())
            .init();
    }
    
    Ok(())
}

fn setup_cors(config: &CorsConfig) -> CorsLayer {
    use tower_http::cors::{Any, CorsLayer};
    use http::{HeaderValue, Method};
    
    let mut cors = CorsLayer::new();
    
    // Setup allowed origins
    if config.allowed_origins.contains(&"*".to_string()) {
        cors = cors.allow_origin(Any);
    } else {
        let origins: Result<Vec<HeaderValue>, _> = config
            .allowed_origins
            .iter()
            .map(|origin| origin.parse())
            .collect();
        
        if let Ok(origins) = origins {
            cors = cors.allow_origin(origins);
        }
    }
    
    // Setup allowed methods
    let methods: Result<Vec<Method>, _> = config
        .allowed_methods
        .iter()
        .map(|method| method.parse())
        .collect();
    
    if let Ok(methods) = methods {
        cors = cors.allow_methods(methods);
    }
    
    // Setup allowed headers
    if config.allowed_headers.contains(&"*".to_string()) {
        cors = cors.allow_headers(Any);
    } else {
        let headers: Result<Vec<HeaderValue>, _> = config
            .allowed_headers
            .iter()
            .map(|header| header.parse())
            .collect();
        
        if let Ok(headers) = headers {
            cors = cors.allow_headers(headers);
        }
    }
    
    cors.max_age(Duration::from_secs(config.max_age_seconds as u64))
}

// Graceful shutdown
async fn shutdown_signal() {
    use tokio::signal;
    
    let ctrl_c = async {
        signal::ctrl_c()
            .await
            .expect("failed to install Ctrl+C handler");
    };
    
    #[cfg(unix)]
    let terminate = async {
        signal::unix::signal(signal::unix::SignalKind::terminate())
            .expect("failed to install signal handler")
            .recv()
            .await;
    };
    
    #[cfg(not(unix))]
    let terminate = std::future::pending::<()>();
    
    tokio::select! {
        _ = ctrl_c => {
            tracing::info!("Received Ctrl+C signal");
        },
        _ = terminate => {
            tracing::info!("Received terminate signal");
        },
    }
    
    tracing::info!("Starting graceful shutdown");
}

Best Practices Summary

  1. RESTful Design: Follow REST principles for resource design and HTTP methods
  2. Input Validation: Validate all input data with clear error messages
  3. Authentication: Implement secure JWT-based authentication
  4. Error Handling: Provide consistent, informative error responses
  5. Documentation: Use OpenAPI/Swagger for API documentation
  6. Testing: Write comprehensive unit and integration tests
  7. Pagination: Implement pagination for list endpoints
  8. Rate Limiting: Protect against abuse with rate limiting
  9. Logging: Implement structured logging for monitoring
  10. Security: Follow security best practices (HTTPS, CORS, validation)
  11. Performance: Use connection pooling and async processing
  12. Monitoring: Implement health checks and metrics

Building REST APIs in Rust provides excellent performance, safety, and maintainability. The strong type system helps catch errors at compile time, while the async ecosystem enables handling many concurrent connections efficiently. Always prioritize security, testing, and documentation for production APIs.