Rust Quick Start

Get started with Bytedocs in Rust applications

Bytedocs for Rust provides compile-time AST analysis for zero runtime overhead, with automatic route detection and type inference.

Supported Frameworks

  • Axum - Ergonomic web framework (primary support)
  • Warp - Composable web framework (planned)
  • Actix-web - Powerful, pragmatic framework (planned)

Installation

Add Bytedocs to your Cargo.toml:

[dependencies]
bytedocs-rs = "0.1"
axum = "0.7"
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }

Quick Start Example

Axum

use axum::{Router, Json, extract::Path};
use bytedocs_rs::{build_docs_from_file, Config, create_docs_router};
use serde::{Serialize, Deserialize};
use std::sync::{Arc, Mutex};
use std::path::PathBuf;

#[derive(Serialize, Deserialize)]
struct User {
    id: i32,
    name: String,
    email: String,
}

#[derive(Deserialize)]
struct CreateUserRequest {
    name: String,
    email: String,
}

async fn get_user(Path(id): Path<i32>) -> Json<User> {
    Json(User {
        id,
        name: "John Doe".to_string(),
        email: "john@example.com".to_string(),
    })
}

async fn create_user(Json(req): Json<CreateUserRequest>) -> Json<User> {
    Json(User {
        id: 1,
        name: req.name,
        email: req.email,
    })
}

#[tokio::main]
async fn main() {
    // Bytedocs configuration
    let config = Config {
        title: "My Rust API".to_string(),
        version: "1.0.0".to_string(),
        description: "My awesome Rust API".to_string(),
        docs_path: "/docs".to_string(),
        ..Default::default()
    };

    // Build documentation from source file
    let source = PathBuf::from("src/main.rs");
    let docs = build_docs_from_file(config.clone(), source)
        .expect("Failed to build docs");

    // Create docs router
    let docs_router = create_docs_router(
        Arc::new(Mutex::new(docs)),
        config
    );

    // Your API routes
    let api_routes = Router::new()
        .route("/users/:id", axum::routing::get(get_user))
        .route("/users", axum::routing::post(create_user));

    // Combine routes
    let app = Router::new()
        .merge(api_routes)
        .merge(docs_router);

    // Run server
    let listener = tokio::net::TcpListener::bind("0.0.0.0:8080")
        .await
        .unwrap();

    println!("Server running on http://localhost:8080");
    println!("Docs available at http://localhost:8080/docs");

    axum::serve(listener, app)
        .await
        .unwrap();
}

Configuration

Basic Configuration

use bytedocs_rs::Config;

let config = Config {
    title: "My API".to_string(),
    version: "1.0.0".to_string(),
    description: "API description".to_string(),
    docs_path: "/docs".to_string(),
    ..Default::default()
};

With Multiple Environments

use bytedocs_rs::{Config, BaseURLOption};

let config = Config {
    title: "My API".to_string(),
    version: "1.0.0".to_string(),
    base_urls: vec![
        BaseURLOption {
            name: "Production".to_string(),
            url: "https://api.example.com".to_string(),
        },
        BaseURLOption {
            name: "Staging".to_string(),
            url: "https://staging.example.com".to_string(),
        },
        BaseURLOption {
            name: "Local".to_string(),
            url: "http://localhost:8080".to_string(),
        },
    ],
    ..Default::default()
};

With Authentication

use bytedocs_rs::{Config, AuthConfig};

let config = Config {
    title: "My API".to_string(),
    version: "1.0.0".to_string(),
    auth_config: Some(AuthConfig {
        enabled: true,
        auth_type: "session".to_string(),
        username: Some("admin".to_string()),
        password: Some("secret".to_string()),
        ..Default::default()
    }),
    ..Default::default()
};

Type Inference

Bytedocs analyzes Rust types at compile time:

Basic Types

use serde::{Serialize, Deserialize};

#[derive(Serialize, Deserialize)]
struct User {
    id: i32,           // Detected as: integer
    name: String,      // Detected as: string
    email: String,     // Detected as: string
    active: bool,      // Detected as: boolean
    age: Option<i32>,  // Detected as: integer (optional)
}

With Documentation

#[derive(Serialize, Deserialize)]
struct User {
    /// User's unique identifier
    id: i32,

    /// Full name of the user
    #[serde(default)]
    name: String,

    /// Email address (must be unique)
    email: String,

    /// Whether the account is active
    #[serde(default = "default_active")]
    active: bool,
}

fn default_active() -> bool {
    true
}

Nested Structures

#[derive(Serialize, Deserialize)]
struct Address {
    street: String,
    city: String,
    country: String,
}

#[derive(Serialize, Deserialize)]
struct User {
    id: i32,
    name: String,
    address: Address,              // Nested object
    addresses: Vec<Address>,        // Array of objects
}

Enums

#[derive(Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
enum UserRole {
    Admin,
    User,
    Guest,
}

#[derive(Serialize, Deserialize)]
struct User {
    id: i32,
    name: String,
    role: UserRole,  // Enum type
}

Result Types

use axum::{http::StatusCode, response::IntoResponse};

// Success response
async fn get_user(Path(id): Path<i32>) -> Result<Json<User>, StatusCode> {
    let user = fetch_user(id)
        .ok_or(StatusCode::NOT_FOUND)?;

    Ok(Json(user))
}

// Custom error type
#[derive(Serialize)]
struct ErrorResponse {
    message: String,
}

async fn get_user_v2(
    Path(id): Path<i32>
) -> Result<Json<User>, (StatusCode, Json<ErrorResponse>)> {
    let user = fetch_user(id)
        .ok_or_else(|| (
            StatusCode::NOT_FOUND,
            Json(ErrorResponse {
                message: "User not found".to_string()
            })
        ))?;

    Ok(Json(user))
}

Axum Extractors

Bytedocs detects common Axum extractors:

Path Parameters

use axum::extract::Path;

// Single parameter
async fn get_user(Path(id): Path<i32>) -> Json<User> {
    // id detected as path parameter
}

// Multiple parameters
async fn get_comment(
    Path((post_id, comment_id)): Path<(i32, i32)>
) -> Json<Comment> {
    // Both parameters detected
}

Query Parameters

use axum::extract::Query;
use serde::Deserialize;

#[derive(Deserialize)]
struct Pagination {
    page: Option<i32>,
    per_page: Option<i32>,
}

async fn get_users(Query(pagination): Query<Pagination>) -> Json<Vec<User>> {
    // page and per_page detected as query parameters
}

Headers

use axum::extract::headers::{Authorization, UserAgent};
use axum::TypedHeader;

async fn protected_route(
    TypedHeader(auth): TypedHeader<Authorization<Bearer>>,
    TypedHeader(user_agent): TypedHeader<UserAgent>,
) -> Json<Data> {
    // Authorization and User-Agent headers detected
}

Request Body

use axum::Json;

#[derive(Deserialize)]
struct CreateUserRequest {
    name: String,
    email: String,
}

async fn create_user(
    Json(req): Json<CreateUserRequest>
) -> Json<User> {
    // CreateUserRequest detected as request body schema
}

Response Types

JSON Response

use axum::Json;

async fn get_user() -> Json<User> {
    // User detected as response schema
    Json(User {
        id: 1,
        name: "John".to_string(),
        email: "john@example.com".to_string(),
    })
}

Status Codes

use axum::http::StatusCode;

async fn create_user(
    Json(req): Json<CreateUserRequest>
) -> (StatusCode, Json<User>) {
    // Status 201 detected
    (
        StatusCode::CREATED,
        Json(User { /* ... */ })
    )
}

Result Types

async fn get_user(
    Path(id): Path<i32>
) -> Result<Json<User>, StatusCode> {
    // Success: 200 with User
    // Error: 404, 500, etc.
    let user = fetch_user(id)
        .ok_or(StatusCode::NOT_FOUND)?;

    Ok(Json(user))
}

Validation

Use validator crate for request validation:

use serde::Deserialize;
use validator::Validate;

#[derive(Deserialize, Validate)]
struct CreateUserRequest {
    #[validate(length(min = 1, max = 100))]
    name: String,

    #[validate(email)]
    email: String,

    #[validate(range(min = 0, max = 150))]
    age: Option<i32>,
}

async fn create_user(
    Json(req): Json<CreateUserRequest>
) -> Result<Json<User>, (StatusCode, String)> {
    // Validate request
    req.validate()
        .map_err(|e| (StatusCode::BAD_REQUEST, e.to_string()))?;

    // Create user...
    Ok(Json(user))
}

Error Handling

Custom Error Type

use axum::{response::IntoResponse, http::StatusCode};

#[derive(Serialize)]
struct ApiError {
    message: String,
    code: String,
}

impl IntoResponse for ApiError {
    fn into_response(self) -> axum::response::Response {
        let status = match self.code.as_str() {
            "NOT_FOUND" => StatusCode::NOT_FOUND,
            "UNAUTHORIZED" => StatusCode::UNAUTHORIZED,
            _ => StatusCode::INTERNAL_SERVER_ERROR,
        };

        (status, Json(self)).into_response()
    }
}

async fn get_user(
    Path(id): Path<i32>
) -> Result<Json<User>, ApiError> {
    let user = fetch_user(id)
        .ok_or(ApiError {
            message: "User not found".to_string(),
            code: "NOT_FOUND".to_string(),
        })?;

    Ok(Json(user))
}

Middleware

Authentication Middleware

use axum::{
    middleware::{self, Next},
    http::{Request, StatusCode},
    response::Response,
};

async fn auth_middleware<B>(
    req: Request<B>,
    next: Next<B>,
) -> Result<Response, StatusCode> {
    let auth_header = req.headers()
        .get("Authorization")
        .and_then(|h| h.to_str().ok());

    match auth_header {
        Some(token) if is_valid_token(token) => {
            Ok(next.run(req).await)
        }
        _ => Err(StatusCode::UNAUTHORIZED),
    }
}

// Apply to routes
let protected_routes = Router::new()
    .route("/users", axum::routing::get(get_users))
    .layer(middleware::from_fn(auth_middleware));

Environment Variables

# .env file
BYTEDOCS_TITLE="My Rust API"
BYTEDOCS_VERSION="1.0.0"
BYTEDOCS_DESCRIPTION="My awesome Rust API"
BYTEDOCS_DOCS_PATH=/docs

# Multiple environments
BYTEDOCS_PRODUCTION_URL=https://api.example.com
BYTEDOCS_STAGING_URL=https://staging.example.com
BYTEDOCS_LOCAL_URL=http://localhost:8080

# Authentication
BYTEDOCS_AUTH_ENABLED=true
BYTEDOCS_AUTH_TYPE=session
BYTEDOCS_AUTH_USERNAME=admin
BYTEDOCS_AUTH_PASSWORD=secret

Using in Configuration

use std::env;
use dotenv::dotenv;
use bytedocs_rs::{Config, AuthConfig};

fn load_config() -> Config {
    dotenv().ok();

    Config {
        title: env::var("BYTEDOCS_TITLE")
            .unwrap_or_else(|_| "My API".to_string()),
        version: env::var("BYTEDOCS_VERSION")
            .unwrap_or_else(|_| "1.0.0".to_string()),
        description: env::var("BYTEDOCS_DESCRIPTION")
            .unwrap_or_default(),
        docs_path: env::var("BYTEDOCS_DOCS_PATH")
            .unwrap_or_else(|_| "/docs".to_string()),
        auth_config: Some(AuthConfig {
            enabled: env::var("BYTEDOCS_AUTH_ENABLED")
                .unwrap_or_default() == "true",
            auth_type: env::var("BYTEDOCS_AUTH_TYPE")
                .unwrap_or_else(|_| "session".to_string()),
            username: env::var("BYTEDOCS_AUTH_USERNAME").ok(),
            password: env::var("BYTEDOCS_AUTH_PASSWORD").ok(),
            ..Default::default()
        }),
        ..Default::default()
    }
}

Production Deployment

Conditional Compilation

#[cfg(not(debug_assertions))]
let docs_enabled = false;

#[cfg(debug_assertions)]
let docs_enabled = true;

if docs_enabled {
    let docs_router = create_docs_router(docs, config);
    app = app.merge(docs_router);
}

Feature Flags

# Cargo.toml
[features]
default = []
docs = ["bytedocs-rs"]

[dependencies]
bytedocs-rs = { version = "0.1", optional = true }
#[cfg(feature = "docs")]
use bytedocs_rs::{build_docs_from_file, create_docs_router};

#[cfg(feature = "docs")]
fn setup_docs(app: Router) -> Router {
    // Documentation setup
    app.merge(docs_router)
}

#[cfg(not(feature = "docs"))]
fn setup_docs(app: Router) -> Router {
    app
}

Best Practices

1. Use Descriptive Types

// ✅ Good - Clear types
#[derive(Serialize, Deserialize)]
struct CreateUserRequest {
    name: String,
    email: String,
}

// ❌ Avoid - Generic types
async fn create_user(Json(data): Json<serde_json::Value>) {
    // Type information lost
}

2. Add Documentation Comments

// ✅ Good - Well documented
/// Get user by ID
///
/// Retrieves a specific user from the database by their unique identifier.
///
/// # Arguments
/// * `id` - The user's unique identifier
///
/// # Returns
/// * `200 OK` - User found and returned
/// * `404 NOT FOUND` - User does not exist
async fn get_user(Path(id): Path<i32>) -> Result<Json<User>, StatusCode> {
    // ...
}

3. Use Result Types

// ✅ Good - Explicit error handling
async fn get_user(
    Path(id): Path<i32>
) -> Result<Json<User>, StatusCode> {
    // Clear success and error types
}

// ❌ Avoid - Unclear error handling
async fn get_user(Path(id): Path<i32>) -> Json<User> {
    // What happens on error?
}

4. Leverage Type System

// ✅ Good - Type-safe
#[derive(Deserialize)]
struct UserId(i32);

async fn get_user(Path(UserId(id)): Path<UserId>) -> Json<User> {
    // Type safety guaranteed
}

Troubleshooting

Documentation Not Generated

  1. Check source file path:
let source = PathBuf::from("src/main.rs");
// Ensure this path is correct
  1. Verify types are public:
// ✅ Public - Will be documented
pub struct User { }

// ❌ Private - Won't be documented
struct User { }

Types Not Detected

Ensure types derive Serialize/Deserialize:

// ✅ Detected
#[derive(Serialize, Deserialize)]
struct User { }

// ❌ Not detected
struct User { }

Build Errors

Ensure all dependencies are included:

[dependencies]
bytedocs-rs = "0.1"
axum = "0.7"
serde = { version = "1", features = ["derive"] }
tokio = { version = "1", features = ["full"] }

Visit Documentation

Start your server and visit:

http://localhost:8080/docs

What's Next?