44 releases
Uses new Rust 2024
| 0.4.7 | Mar 12, 2026 |
|---|---|
| 0.4.0 | Feb 24, 2026 |
| 0.2.4 | Dec 19, 2025 |
| 0.2.0 | Oct 5, 2025 |
| 0.1.0 | Dec 11, 2024 |
#386 in HTTP server
88KB
431 lines
mandolin
Mandolin is a tool to generate server and client code from OpenAPI specifications (JSON, and optionally YAML). It currently supports:
Mandolin adopts a "Logic in Templates" design philosophy, where Rust handles data preparation and $ref resolution, while templates handle the code assembly.
Features
| Feature | Default | Description |
|---|---|---|
yaml |
off | Enable YAML input support via serde_yaml. When disabled, all input (regardless of file extension) is parsed as JSON. |
mandolin_client |
off | Enable ApiClient in the generated code. Uses reqwest to call the API as an HTTP client. |
Enable YAML support:
# Cargo.toml
[dependencies]
mandolin = { version = "...", features = ["yaml"] }
Or when installing the CLI:
$ cargo install mandolin --features yaml
Getting started
Install mandolin from source:
$ cargo install --path .
Render axum server code using builtin "RUST_AXUM" template:
$ mandolin -i ./openapi/openapi_plant.yaml -t RUST_AXUM -o ./examples/example_server.rs
Using pipes:
$ cat openapi.json | mandolin -i - -t TYPESCRIPT_HONO > ./examples/server.ts
You can also use mandolin as library
Mandolin exposes a simple API mandolin::environment that returns a configured Minijinja environment.
use mandolin;
use openapiv3::OpenAPI;
fn main() {
// 1. Read OpenAPI file (use openapi_loader to handle JSON / YAML transparently)
let f = std::fs::File::open("./openapi/openapi_petstore.json").unwrap();
let api: OpenAPI = mandolin::openapi_loader::load(f, "openapi_petstore.json").unwrap();
// 2. Create environment
let env = mandolin::environment(api).unwrap();
// 3. Render
let output = env.get_template("RUST_AXUM").unwrap().render(0).unwrap();
std::fs::write("examples/generated_server.rs", output).unwrap();
}
Note: To load YAML files, enable the
yamlfeature. Without it,openapi_loader::loadfalls back to JSON parsing regardless of the file extension.
Example of generated code
The generated code defines two traits: ApiInterface (business logic) and ApiInterfaceAxum (axum-specific behavior). You must implement both to use axum_router.
// This is generated by mandolin
use axum;
use serde;
use std::future::Future;
pub trait ApiInterface {
// get /device/{key}
fn device_get(&self, key: String) -> impl Future<Output = DeviceGetResponse> + Send {
async { DeviceGetResponse::Status404 }
}
// post /device
fn device_create(&self, req: DeviceCreateRequest) -> impl Future<Output = DeviceCreateResponse> + Send {
async { DeviceCreateResponse::Status201(Default::default()) }
}
}
// ... Request/Response structs and Router definition follows ...
Running the generated server with your implementation
You can import the generated module and implement the trait to build your server.
mod generated; // The file generated by mandolin
use generated::*;
use axum::{Router, serve};
use tokio::net::TcpListener;
struct MyServer {
db_url: String,
}
// Implement the business logic
impl ApiInterface for MyServer {
async fn device_get(&self, req: DeviceGetRequest) -> DeviceGetResponse {
// Your implementation here...
DeviceGetResponse::Status200(Device {
key: req.key,
name: "example-device".to_string(),
..Default::default()
})
}
}
// Required to use axum_router. Override methods for axum-specific behavior.
impl ApiInterfaceAxum for MyServer {
// async fn authorize(&self, _req: http::Request<()>) -> Result<AuthContext, String> { Ok(Default::default()) }
// GET /device/{key}
// async fn device_get(&self, _raw: http::Request<()>, req: DeviceGetRequest) -> axum::response::Response { ... }
// POST /device
// async fn device_create(&self, _raw: http::Request<()>, req: DeviceCreateRequest) -> axum::response::Response { ... }
}
#[tokio::main]
async fn main() {
let api = MyServer { db_url: "postgres://...".to_string() };
// 'axum_router' is generated by mandolin
let app = axum_router(api);
let listener = TcpListener::bind("0.0.0.0:8080").await.unwrap();
serve(listener, app).await.unwrap();
}
Using the generated client
Enable the mandolin_client feature in your generated crate's Cargo.toml:
[features]
mandolin_client = ["dep:reqwest"]
[dependencies]
reqwest = { version = "*", features = ["json"], optional = true }
The generated code includes ApiClient, which implements ApiInterface using reqwest.
You can use it anywhere a server implementation would be used.
mod generated; // The file generated by mandolin
use generated::*;
#[tokio::main]
async fn main() {
let client = ApiClient::new("http://localhost:8080/api");
// Call the API — same interface as the server implementation
let response = client.device_get(DeviceGetRequest {
key: "my-device".to_string(),
}).await;
match response {
DeviceGetResponse::Status200(device) => println!("{:?}", device),
DeviceGetResponse::Status404 => println!("not found"),
DeviceGetResponse::Error(msg) => println!("error: {msg}"),
_ => {}
}
}
DeviceGetResponse::Error(String) is returned on network errors, unexpected status codes, or deserialization failures.
Custom Templates
You can easily use your own templates.
Dependencies are minimized, and helpers like include_ref are no longer needed because $ref is pre-resolved.
use mandolin;
use openapiv3::OpenAPI;
use std::fs;
fn main() {
let f = fs::File::open("./openapi/openapi.json").unwrap();
let api: OpenAPI = mandolin::openapi_loader::load(f, "openapi.json").unwrap();
let mut env = mandolin::environment(api).unwrap();
// Add your custom template
let content = fs::read_to_string("./my_templates/custom_rust.template").unwrap();
env.add_template("CUSTOM_RUST", &content).unwrap();
let output = env.get_template("CUSTOM_RUST").unwrap().render(0).unwrap();
fs::write("server.rs", output).unwrap();
}
Version History
-
0.4.5
- Removed blanket
impl<T: ApiInterface + Sync> ApiInterfaceAxum for T. Users must now explicitly implementApiInterfaceAxum(empty impl is fine) to useaxum_router. This allows overridingauthorizeand per-operation methods.
- Removed blanket
-
0.4.2-alpha.1
- Added
ApiClientto generated code: implementsApiInterfaceusingreqwest, enabled viamandolin_clientfeature. - Added
Error(String)variant to all generated response enums for network/deserialization errors. ApiInterfaceis now fully framework-agnostic (no axum dependency).- Axum-specific logic moved to
ApiInterfaceAxum(server-only). authorizemoved fromApiInterfacetoApiInterfaceAxum.IntoResponseimplemented per response enum (replaces inline match in router).
- Added
-
0.4.0-alpha.6
- Added
yamloptional feature (disabled by default). YAML input support viaserde_yamlis now opt-in. - Added
openapi_loadermodule:mandolin::openapi_load(reader)andmandolin::openapi_parse_str(s)as top-level helpers. - When the
yamlfeature is enabled, YAML parsing is attempted first and falls back to JSON on failure.
- Added
-
0.4.0-alpha.1
- Major Re-architecture: "Logic in Templates".
- Moved logic from Rust to templates.
$refis now pre-resolved in Rust.- Templates are consolidated into single files (no more
includehell). - TypeScript (Hono) support improved.
-
0.2.5
- Improve
rust_axum.templateto correctly setContent-Typeheader
- Improve
-
0.2.4
- Internal bug fixes and improvements to response handling
-
0.2.3 add binary target
-
0.2.2 Fix bugs about no content response
-
0.2.1 Add impl AsRef<axum::http::Requestaxum::body::Body> for Requests
-
0.2.0
- support parse multipart/form-data
- support catch-all path arguments
-
0.1.13 support date schema
-
0.1.12 add target "TYPESCRIPT_HONO"
-
0.1.0 publish
My favorite mandolin music
- 月に舞う/武藤理恵 https://youtu.be/OVKkRj0di2I
- Suite Spagnola/C.Mandonico https://youtu.be/fCkcP_cuneUU
Dependencies
~5–7.5MB
~129K SLoC