#openapi-generator #axum #axum-server #openapi #generator

bin+lib mandolin

Input openapi.json/yaml, output server source code in rust

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

MIT license

88KB
431 lines

mandolin

GitHub License Crates.io

Mandolin is a tool to generate server and client code from OpenAPI specifications (JSON, and optionally YAML). It currently supports:

  • Rust server: axum
  • Rust client: reqwest (opt-in via mandolin_client feature)
  • TypeScript: Hono

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 yaml feature. Without it, openapi_loader::load falls 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 implement ApiInterfaceAxum (empty impl is fine) to use axum_router. This allows overriding authorize and per-operation methods.
  • 0.4.2-alpha.1

    • Added ApiClient to generated code: implements ApiInterface using reqwest, enabled via mandolin_client feature.
    • Added Error(String) variant to all generated response enums for network/deserialization errors.
    • ApiInterface is now fully framework-agnostic (no axum dependency).
    • Axum-specific logic moved to ApiInterfaceAxum (server-only).
    • authorize moved from ApiInterface to ApiInterfaceAxum.
    • IntoResponse implemented per response enum (replaces inline match in router).
  • 0.4.0-alpha.6

    • Added yaml optional feature (disabled by default). YAML input support via serde_yaml is now opt-in.
    • Added openapi_loader module: mandolin::openapi_load(reader) and mandolin::openapi_parse_str(s) as top-level helpers.
    • When the yaml feature is enabled, YAML parsing is attempted first and falls back to JSON on failure.
  • 0.4.0-alpha.1

    • Major Re-architecture: "Logic in Templates".
    • Moved logic from Rust to templates.
    • $ref is now pre-resolved in Rust.
    • Templates are consolidated into single files (no more include hell).
    • TypeScript (Hono) support improved.
  • 0.2.5

    • Improve rust_axum.template to correctly set Content-Type header
  • 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

Dependencies

~5–7.5MB
~129K SLoC