Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Implementing a server

#[openapi_trait::axum(...)] generates a {Mod}Api trait with one async method per operation, plus a router() method that wires it to an axum::Router.

#[openapi_trait::axum("examples/pets.openapi.yaml")]
pub mod pets {}

Implement the trait

impl pets::PetsApi for PetServer {
    type Error = pets::NotImplemented;

    async fn list_pets(
        &self,
        ctx: pets::list_pets::Context,
    ) -> Result<pets::list_pets::Response, Self::Error> {
        let pets::list_pets::Context { req, .. } = ctx;
        let store = self.pets.lock().unwrap();
        let limit = req
            .limit
            .map_or(usize::MAX, |n| usize::try_from(n).unwrap_or(0));
        Ok(pets::list_pets::Response::Status200(
            store.iter().take(limit).cloned().collect(),
        ))
    }

    async fn create_pet(
        &self,
        ctx: pets::create_pet::Context,
    ) -> Result<pets::create_pet::Response, Self::Error> {
        let pets::create_pet::Context { req, auth, .. } = ctx;
        // The macro only extracts the token; checking it is up to you.
        if auth.0 != "secret" {
            return Ok(pets::create_pet::Response::Status401);
        }
        let mut store = self.pets.lock().unwrap();
        let pet = pets::Pet {
            id: i64::try_from(store.len()).unwrap_or(i64::MAX) + 1,
            name: req.body.name,
            status: req.body.status,
        };
        store.push(pet.clone());
        drop(store);
        Ok(pets::create_pet::Response::Status201(pet))
    }

    async fn get_pet(
        &self,
        ctx: pets::get_pet::Context,
    ) -> Result<pets::get_pet::Response, Self::Error> {
        let pets::get_pet::Context { req, .. } = ctx;
        let store = self.pets.lock().unwrap();
        Ok(store
            .iter()
            .find(|pet| pet.id == req.pet_id)
            .cloned()
            .map_or(
                pets::get_pet::Response::Status404,
                pets::get_pet::Response::Status200,
            ))
    }
}

Each method receives one argument, the operation’s {operation}::Context (see below), and returns Result<{operation}::Response, Self::Error>. The response enum has one variant per declared status code (Status200(Pet), Status404, …), and each variant implements IntoResponse with the matching status and body.

A default response becomes a Default variant that carries its own status, so one response can describe every error. If getPet declared a default response with an Error schema as its body:

Ok(get_pet::Response::Default {
    status: openapi_trait::http::StatusCode::UNPROCESSABLE_ENTITY,
    body: pets::Error { code: 422, message: "bad id".into() },
})

openapi_trait::http::StatusCode is the same type as axum::http::StatusCode. Response headers declared on default are fields of the variant, next to body.

The handler’s context

{operation}::Context holds everything the router extracted for one request:

FieldTypePresent
req{operation}::Request: path, query and header parameters as typed fields, and the request body as bodyalways
auththe credentials, see Securityonly if the operation is secured
headersaxum::http::HeaderMap, for anything the spec doesn’t modelalways

Spec parameters always live in req, never on the context itself, so a parameter called auth or headers can’t clash with these fields.

There are several ways to get at the fields. They all compile to the same thing, so pick whichever reads best for the handler.

Field access

Read and move fields straight off ctx. Moving one field out, such as the body, leaves the others usable:

async fn create_pet(
    &self,
    ctx: pets::create_pet::Context,
) -> Result<pets::create_pet::Response, Self::Error> {
    let body = ctx.req.body;                     // moves the body out
    let trace = ctx.headers.get("x-trace-id");   // other fields still work
    if ctx.auth.0 != "secret" { /* … */ }
    // …
}

Destructuring in the body

Name only the fields you need and end the pattern with ..:

let pets::create_pet::Context { req, auth, .. } = ctx;

Destructuring in the signature

A pattern can replace the argument name. This keeps the body free of unpacking, at the cost of a longer signature:

async fn get_pet(
    &self,
    pets::get_pet::Context { req, .. }: pets::get_pet::Context,
) -> Result<pets::get_pet::Response, Self::Error> {
    // `req` is in scope here
}

Nested destructuring

Patterns nest, so you can unpack the request’s parameters in the same step:

let pets::list_pets::Context {
    req: pets::list_pets::Request { limit, .. },
    headers,
    ..
} = ctx;

Request has no private field, so .. is optional inside it. Leave it in anyway: without it, the pattern breaks when the spec gains a parameter.

Why .. is required

Context has a private field. Outside the generated module, a pattern without .. is rejected (pattern requires .. due to inaccessible fields), and so is a struct literal. That’s deliberate: it lets new fields, such as the request URI, be added to Context without breaking your handlers.

No Deref to the request

Context doesn’t implement Deref<Target = Request>, so you write ctx.req.pet_id, not ctx.pet_id. Deref would save typing .req when reading a field, but nothing else:

With DerefResult
ctx.pet_id (read a Copy field)works
&ctx.body (borrow)works
let body = ctx.body; (move)error: cannot move out of a dereference
let Context { pet_id, .. } = ctx; (pattern)error: patterns never look through Deref
ctx.headers when the spec has a headers parametercompiles, but silently reads the context’s field, not the parameter

The last row is the reason it’s left out: Deref would let a spec parameter be shadowed without any warning. With ctx.req.… it’s always clear which one you mean.

Calling a handler directly

To call a handler without going through HTTP, for example in a unit test, build a context with Context::new. It takes the request, then the credentials (only for secured operations), then the headers:

let ctx = pets::get_pet::Context::new(
    pets::get_pet::Request { pet_id: 1 },
    pets::BearerAuth("secret".into()),
    axum::http::HeaderMap::new(),
);
let response = PetServer::default().get_pet(ctx).await?;

Call it through the trait (use pets::PetsApi as _;). The method has no inherent version.

Application state

Keep application state in the type that implements the trait, and reach it through &self. The router wraps the implementation in an Arc, so it is shared, not cloned, per request.

/// The type that implements the generated API trait. Application state lives
/// in its fields; every handler reaches it through `&self`.
#[derive(Clone, Default)]
struct PetServer {
    /// In-memory pet storage.
    pets: Arc<Mutex<Vec<pets::Pet>>>,
}

Start the router

#[tokio::main]
async fn main() {
    use pets::PetsApi as _;

    let app: axum::Router = PetServer::default().router();
    let listener = tokio::net::TcpListener::bind("127.0.0.1:3000")
        .await
        .unwrap();
    println!("listening on http://{}", listener.local_addr().unwrap());
    axum::serve(listener, app).await.unwrap();
}

router() returns a plain axum::Router, so you can .merge() it, .nest() it, or add tower layers like any other router. To combine it with routes that use axum state, convert it with .with_state(()), which yields a Router<S> for any S:

let app = axum::Router::new()
    .route("/health", get(health))
    .merge(PetServer::default().router().with_state(()))
    .with_state(app_state);

Unimplemented operations and errors

Every trait method has a default body that returns Err(NotImplemented.into()). You can implement operations one at a time; routes you haven’t implemented yet answer 500 not implemented.

Self::Error must implement axum::response::IntoResponse and From<pets::NotImplemented>. Using pets::NotImplemented itself is the simplest choice. For real error handling, use your own type:

enum AppError {
    NotImplemented,
    Database(sqlx::Error),
}

impl From<pets::NotImplemented> for AppError {
    fn from(_: pets::NotImplemented) -> Self { Self::NotImplemented }
}

impl axum::response::IntoResponse for AppError {
    fn into_response(self) -> axum::response::Response {
        // map to a status code and body of your choice
        todo!()
    }
}

An Err is for failures outside the contract. Responses the spec declares, such as 404, belong in the Ok variant (Ok(get_pet::Response::Status404)).

Binary bodies

When an operation takes or returns application/octet-stream (or another binary media type), the body is a streamed axum::body::Body rather than a Vec<u8>, so large uploads and downloads don’t have to fit in memory. See OpenAPI support.