oxapi
Generate type-safe Rust server stubs from OpenAPI specs.
Note: This crate was initially vibe-coded because it's an uninteresting problem. Use at your own risk. Further
updates will likely be by hand.
Usage
Add to your Cargo.toml:
[dependencies]
oxapi = { git = "..." }
axum = "0.8"
tokio = { version = "1", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
Example
Given an OpenAPI spec, define a trait with your handlers:
use axum::{Router, extract::{State, Path, Json}};
#[oxapi::oxapi(axum, "api.json")]
trait MyApi {
#[oxapi(map)]
fn map_routes(router: Router<AppState>) -> Router<AppState>;
#[oxapi(get, "/users/{id}")]
async fn get_user(state: State<AppState>, id: Path<_>);
#[oxapi(post, "/users")]
async fn create_user(state: State<AppState>, body: Json<_>);
}
The macro generates:
- A
my_api_types module with all types from the spec
- Response enums (
GetUserOk, GetUserErr) that implement IntoResponse
- Filled-in type parameters for
Path<_>, Query<_>, Json<_>
- Return types as
Result<{Op}Ok, {Op}Err>
Implement the trait:
use my_api_types::*;
struct MyApiImpl;
impl MyApi for MyApiImpl {
async fn get_user(
State(state): State<AppState>,
Path(id): Path<i64>,
) -> Result<GetUserOk, GetUserErr> {
match state.users.get(&id) {
Some(user) => Ok(GetUserOk::Status200(user.clone())),
None => Err(GetUserErr::Status404),
}
}
async fn create_user(
State(state): State<AppState>,
Json(user): Json<User>,
) -> Result<CreateUserOk, CreateUserErr> {
}
}
#[tokio::main]
async fn main() {
let state = AppState::new();
let app = MyApiImpl::map_routes(Router::new()).with_state(state);
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}
Splitting by Responsibility
For larger APIs, use a module to split operations across multiple traits:
#[oxapi::oxapi(axum, "api.json")]
mod api {
trait UserService {
#[oxapi(map)]
fn map_routes(router: Router<UserState>) -> Router<UserState>;
#[oxapi(get, "/users/{id}")]
async fn get_user(state: State<UserState>, id: Path<_>);
}
trait OrderService {
#[oxapi(map)]
fn map_routes(router: Router<OrderState>) -> Router<OrderState>;
#[oxapi(get, "/orders/{id}")]
async fn get_order(state: State<OrderState>, id: Path<_>);
}
}
use api::{types::*, UserService, OrderService};
Each trait can have its own state type. The macro validates that all spec operations are covered exactly once across all traits.
Compose the routers:
let app = Router::new()
.merge(UserServiceImpl::map_routes(Router::new()).with_state(user_state))
.merge(OrderServiceImpl::map_routes(Router::new()).with_state(order_state));
Attributes
#[oxapi(map)] - Marks the route mapping function (body auto-generated)
#[oxapi(get, "/path")] - GET handler
#[oxapi(post, "/path")] - POST handler
#[oxapi(put, "/path")] - PUT handler
#[oxapi(delete, "/path")] - DELETE handler
#[oxapi(patch, "/path")] - PATCH handler
Type Elision
Use _ for types the macro should fill from the spec:
async fn get_user(id: Path<_>, body: Json<_>);
For AI Agents
See llms.txt for comprehensive documentation optimized for LLMs.
License
LGPL-3.0