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
- Check source file path:
let source = PathBuf::from("src/main.rs");
// Ensure this path is correct
- 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?
- Core Concepts - Understand how Bytedocs works
- Configuration - Advanced configuration
- Examples - Complete example projects
On this page