| Crates.io | stonehm |
| lib.rs | stonehm |
| version | 0.1.4 |
| created_at | 2025-09-29 21:01:33.432638+00 |
| updated_at | 2025-10-28 19:50:59.097477+00 |
| description | Automatic OpenAPI 3.0 generation for Axum applications |
| homepage | |
| repository | https://github.com/melito/stonehm |
| max_upload_size | |
| id | 1860195 |
| size | 105,270 |
stonehm automatically generates comprehensive OpenAPI 3.0 specifications for Axum web applications by analyzing handler functions and their documentation. The core principle is "documentation is the spec" - write clear, natural documentation and get complete OpenAPI specs automatically.
Result<T, E> return typesaxum::RouterAdd to your Cargo.toml:
[dependencies]
stonehm = "0.1"
stonehm-macros = "0.1"
axum = "0.7"
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
serde = { version = "1.0", features = ["derive"] }
use axum::{Json, extract::Path};
use serde::{Serialize, Deserialize};
use stonehm::{api_router, api_handler};
use stonehm_macros::{StonehmSchema, api_error};
// Define your data types
#[derive(Serialize, StonehmSchema)]
struct User {
id: u32,
name: String,
email: String,
}
#[api_error]
enum ApiError {
/// 404: User not found
UserNotFound { id: u32 },
/// 500: Internal server error
DatabaseError,
}
/// Get user by ID
///
/// Retrieves a user's information using their unique identifier.
/// Returns detailed user data including name and email.
#[api_handler]
async fn get_user(Path(id): Path<u32>) -> Result<Json<User>, ApiError> {
Ok(Json(User {
id,
name: format!("User {}", id),
email: format!("user{}@example.com", id),
}))
}
#[tokio::main]
async fn main() {
let app = api_router!("User API", "1.0.0")
.get("/users/:id", get_user)
.with_openapi_routes() // Adds /openapi.json and /openapi.yaml
.into_router();
let listener = tokio::net::TcpListener::bind("127.0.0.1:3000").await.unwrap();
println!("Server running on http://127.0.0.1:3000");
println!("OpenAPI spec: http://127.0.0.1:3000/openapi.json");
axum::serve(listener, app).await.unwrap();
}
That's it! You now have a fully documented API with automatic OpenAPI generation.
Automatic OpenAPI generation includes:
stonehm supports three documentation approaches to fit different needs:
Let stonehm infer everything from your code structure:
/// Get user profile
///
/// Retrieves the current user's profile information.
#[api_handler]
async fn get_profile() -> Result<Json<User>, ApiError> {
Ok(Json(User::default()))
}
Automatically generates:
### 2. Structured Documentation
Add detailed parameter and response documentation:
```rust
/// Update user profile
///
/// Updates the user's profile information. Only provided fields
/// will be updated, others remain unchanged.
///
/// # Parameters
/// - id (path): The user's unique identifier
/// - version (query): API version to use
/// - authorization (header): Bearer token for authentication
///
/// # Request Body
/// Content-Type: application/json
/// User update data with optional fields for name, email, and preferences.
///
/// # Responses
/// - 200: User successfully updated
/// - 400: Invalid user data provided
/// - 401: Authentication required
/// - 404: User not found
/// - 422: Validation failed
#[api_handler]
async fn update_profile(
Path(id): Path<u32>,
Json(request): Json<UpdateUserRequest>
) -> Result<Json<User>, ApiError> {
Ok(Json(User::default()))
}
For complex APIs requiring detailed error schemas:
/// Delete user account
///
/// Permanently removes a user account and all associated data.
/// This action cannot be undone.
///
/// # Parameters
/// - id (path): The unique user identifier to delete
///
/// # Responses
/// - 204: User successfully deleted
/// - 404:
/// description: User not found
/// content:
/// application/json:
/// schema: NotFoundError
/// - 403:
/// description: Insufficient permissions to delete user
/// content:
/// application/json:
/// schema: PermissionError
/// - 409:
/// description: Cannot delete user with active subscriptions
/// content:
/// application/json:
/// schema: ConflictError
#[api_handler]
async fn delete_user(Path(id): Path<u32>) -> Result<(), ApiError> {
Ok(())
}
stonehm uses the StonehmSchema derive macro for automatic schema generation:
use serde::{Serialize, Deserialize};
use stonehm_macros::StonehmSchema;
#[derive(Serialize, Deserialize, StonehmSchema)]
struct CreateUserRequest {
name: String,
email: String,
age: Option<u32>,
preferences: UserPreferences,
}
#[derive(Serialize, StonehmSchema)]
struct UserResponse {
id: u32,
name: String,
email: String,
created_at: String,
is_active: bool,
}
#[api_error]
enum ApiError {
/// 400: Invalid input provided
InvalidInput { field: String, message: String },
/// 404: User not found
UserNotFound { id: u32 },
/// 409: Email already exists
EmailAlreadyExists { email: String },
/// 500: Internal server error
DatabaseError,
/// 422: Validation failed
ValidationFailed,
}
Supported types: All primitive types, Option<T>, Vec<T>, nested structs, and enums.
use stonehm::api_router;
#[tokio::main]
async fn main() {
let app = api_router!("My API", "1.0.0")
.get("/users/:id", get_user)
.post("/users", create_user)
.put("/users/:id", update_user)
.delete("/users/:id", delete_user)
.with_openapi_routes() // Adds /openapi.json and /openapi.yaml
.into_router();
let listener = tokio::net::TcpListener::bind("127.0.0.1:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}
// Default endpoints
.with_openapi_routes() // Creates /openapi.json and /openapi.yaml
// Custom prefix
.with_openapi_routes_prefix("/api/docs") // Creates /api/docs.json and /api/docs.yaml
// Custom paths
.with_openapi_routes_prefix("/v1/spec") // Creates /v1/spec.json and /v1/spec.yaml
/// Brief one-line summary
///
/// Detailed description that can span multiple paragraphs.
/// This becomes the OpenAPI description field.
/// # Parameters
/// - id (path): The unique user identifier
/// - page (query): Page number for pagination
/// - limit (query): Maximum results per page
/// - authorization (header): Bearer token for authentication
/// # Request Body
/// Content-Type: application/json
/// Detailed description of the expected request body structure
/// and any validation requirements.
Simple format (covers most use cases):
/// # Responses
/// - 200: User successfully created
/// - 400: Invalid user data provided
/// - 409: Email address already exists
Elaborate format (for detailed error documentation):
/// # Responses
/// - 201: User successfully created
/// - 400:
/// description: Validation failed
/// content:
/// application/json:
/// schema: ValidationError
/// - 409:
/// description: Email already exists
/// content:
/// application/json:
/// schema: ConflictError
Return Result<Json<T>, E> to get automatic error responses:
/// Recommended - Automatic error handling
#[api_handler]
async fn get_user() -> Result<Json<User>, ApiError> {
Ok(Json(User { id: 1, name: "John".to_string(), email: "john@example.com".to_string() }))
}
/// Manual - Requires explicit response documentation
#[api_handler]
async fn get_user_manual() -> Json<User> {
Json(User { id: 1, name: "John".to_string(), email: "john@example.com".to_string() })
}
Generated OpenAPI for automatic error handling:
responses:
'200':
description: Success
content:
application/json:
schema:
$ref: '#/components/schemas/User'
'400':
description: Bad Request
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
'500':
description: Internal Server Error
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
Manual documentation requires explicit responses:
responses:
'200':
description: Success
content:
application/json:
schema:
$ref: '#/components/schemas/User'
use stonehm_macros::api_error;
#[api_error]
enum ApiError {
/// 404: User not found
UserNotFound { id: u32 },
/// 400: Validation failed
ValidationError { field: String, message: String },
/// 500: Internal server error
DatabaseError,
}
The api_error macro automatically generates IntoResponse, Serialize, and StonehmSchema implementations, eliminating all boilerplate.
Focus on business logic, not OpenAPI details:
/// Good - describes what the endpoint does
/// Creates a new user account with email verification
/// Avoid - implementation details
/// Returns HTTP 201 with application/json content-type
/// Simple for basic APIs
/// # Responses
/// - 200: Success
/// - 400: Bad request
/// Elaborate for complex error handling
/// # Responses
/// - 400:
/// description: Validation failed
/// content:
/// application/json:
/// schema: ValidationError
| Return Type | Automatic Behavior | When to Use Manual |
|---|---|---|
Json<T> |
200 response with T schema | Simple endpoints |
Result<Json<T>, E> |
200 with T schema 400, 500 with E schema |
Most endpoints (recommended) |
() or StatusCode |
200 empty response | DELETE operations |
| Custom types | Depends on implementation | Advanced use cases |
Q: My error responses aren't appearing
A: Ensure your function returns Result<Json<T>, E> and E implements IntoResponse.
Q: Schemas aren't in the OpenAPI spec
A: Add #[derive(StonehmSchema)] to your types and use them in function signatures.
Q: Path parameters not documented
A: Add them to the # Parameters section with (path) type specification.
Q: Custom response schemas not working
A: Use the elaborate response format with explicit schema references.
| Macro | Purpose | Example |
|---|---|---|
api_router!(title, version) |
Create documented router | api_router!("My API", "1.0.0") |
#[api_handler] |
Mark handler for documentation | #[api_handler] async fn get_user() {} |
#[derive(StonehmSchema)] |
Generate JSON schema | #[derive(Serialize, StonehmSchema)] struct User {} |
let app = api_router!("API", "1.0.0")
.get("/users", list_users) // GET route
.post("/users", create_user) // POST route
.put("/users/:id", update_user) // PUT route
.delete("/users/:id", delete_user) // DELETE route
.patch("/users/:id", patch_user) // PATCH route
.with_openapi_routes() // Add OpenAPI endpoints
.into_router(); // Convert to axum::Router
| Method | Creates | Description |
|---|---|---|
.with_openapi_routes() |
/openapi.json/openapi.yaml |
Default OpenAPI endpoints |
.with_openapi_routes_prefix("/api") |
/api.json/api.yaml |
Custom prefix |
| Rust Type | OpenAPI Response | Automatic Errors |
|---|---|---|
Json<T> |
200 with T schema | None |
Result<Json<T>, E> |
200 with T schema | 400, 500 with E schema |
() |
204 No Content | None |
StatusCode |
Custom status | None |
use axum::{Json, extract::{Path, Query}};
use serde::{Serialize, Deserialize};
use stonehm::{api_router, api_handler};
use stonehm_macros::StonehmSchema;
#[derive(Serialize, Deserialize, StonehmSchema)]
struct User {
id: u32,
name: String,
email: String,
created_at: String,
}
#[derive(Deserialize, StonehmSchema)]
struct CreateUserRequest {
name: String,
email: String,
}
#[derive(Deserialize)]
struct UserQuery {
page: Option<u32>,
limit: Option<u32>,
}
#[api_error]
enum ApiError {
/// 404: User not found
UserNotFound { id: u32 },
/// 400: Validation failed
ValidationError { field: String, message: String },
/// 500: Internal server error
DatabaseError,
}
/// List users with pagination
///
/// Retrieves a paginated list of users from the database.
///
/// # Parameters
/// - page (query): Page number (default: 1)
/// - limit (query): Users per page (default: 10, max: 100)
#[api_handler]
async fn list_users(Query(query): Query<UserQuery>) -> Result<Json<Vec<User>>, ApiError> {
Ok(Json(vec![]))
}
/// Get user by ID
///
/// Retrieves detailed user information by ID.
#[api_handler]
async fn get_user(Path(id): Path<u32>) -> Result<Json<User>, ApiError> {
Ok(Json(User {
id,
name: "John Doe".to_string(),
email: "john@example.com".to_string(),
created_at: "2024-01-01T00:00:00Z".to_string(),
}))
}
/// Create new user
///
/// Creates a new user account with the provided information.
///
/// # Request Body
/// Content-Type: application/json
/// User creation data with required name and email fields.
#[api_handler]
async fn create_user(Json(req): Json<CreateUserRequest>) -> Result<Json<User>, ApiError> {
Ok(Json(User {
id: 42,
name: req.name,
email: req.email,
created_at: "2024-01-01T00:00:00Z".to_string(),
}))
}
#[tokio::main]
async fn main() {
let app = api_router!("User Management API", "1.0.0")
.get("/users", list_users)
.get("/users/:id", get_user)
.post("/users", create_user)
.with_openapi_routes()
.into_router();
let listener = tokio::net::TcpListener::bind("127.0.0.1:3000").await.unwrap();
println!("Server running on http://127.0.0.1:3000");
println!("OpenAPI spec: http://127.0.0.1:3000/openapi.json");
axum::serve(listener, app).await.unwrap();
}
# Clone the repository
git clone https://github.com/melito/stonehm.git
cd stonehm
# Run the example server
cargo run -p hello_world
# Test OpenAPI generation
cargo run -p hello_world -- --test-schema
# Use default endpoints (/openapi.json, /openapi.yaml)
cargo run -p hello_world -- --default
# Generate and view the OpenAPI spec
cargo run -p hello_world -- --test-schema | jq '.'
# Check specific endpoints
cargo run -p hello_world -- --test-schema | jq '.paths."/users".post'
# View all schemas
cargo run -p hello_world -- --test-schema | jq '.components.schemas'
We welcome contributions! Please feel free to submit issues and pull requests.
# Run tests
cargo test
# Check formatting
cargo fmt --check
# Run clippy
cargo clippy -- -D warnings
# Test all examples
cargo test --workspace
This project is licensed under the MIT License - see the LICENSE file for details.