Working with OpenAPI using Rust
Tags: rust, webdev, tutorial, programming
Published: 2024-04-04 15:24:07


Hello world! In this article we’re going to talk about how you can make the most of OpenAPI with Rust, by learning all the different ways we can use OpenAPI in a Rust context. By the end of this article, you'll learn the following:
- Adding OpenAPI to a Rust web service
- How to generate API client libraries from OpenAPI specifications
- Working with the OpenAPI spec directly

## What is OpenAPI?

From Swagger.io:

> The OpenAPI specification (OAS) defines a standard, language-agnostic interface to HTTP APIs which allows both humans and computers to discover and understand the capabilities of the service without access to source code.

No matter what language you’re using, OpenAPI allows you to easily read the specification and understand how to use an API without needing specific documentation.

Here is a simple example of what an OpenAPI specification file may look like:

```yaml
swagger: "3.0"
info:
  version: "1.0"
  title: "Hello World API"
paths:
  /hello/{user}:
    get:
      description: Returns a greeting to the user!
      parameters:
        - name: user
          in: path
          type: string
          required: true
          description: The name of the user to greet.
      responses:
        200:
          description: Returns the greeting.
          schema:
            type: string
        400:
          description: Invalid characters in "user" were provided.
```

There are several benefits of using OpenAPI:

- You can improve cross-team collaboration by allowing teammates to quickly experiment with endpoints by providing a frontend
- You can quickly get an understanding of endpoints that your teammates have made
- It’s widely used, so you can get an understanding of official APIs that utilise it much faster
- We can generate code from it as it’s machine parseable!

## Adding OpenAPI to a Rust API

### utoipa

Adding an OpenAPI specification to a Rust API can be done with the `utoipa` family of crates. `utoipa` is a crate that primarily uses macros to set up the OpenAPI specification. There is also support for frontend GUIs like Swagger UI, Redoc and Rapidoc that allow you to visualise working with your API

A simple Axum example that shows a JSON representation of your OpenAPI specification looks like this:

```rust
use std::net::SocketAddr;

use axum::{routing::get, Json};
use utoipa::OpenApi;

#[derive(OpenApi)]
#[openapi(paths(openapi))]
struct ApiDoc;

/// Return JSON version of an OpenAPI schema
#[utoipa::path(
    get,
    path = "/api-docs/openapi.json",
    responses(
        (status = 200, description = "JSON file", body = ())
    )
)]
async fn openapi() -> Json<utoipa::openapi::OpenApi> {
    Json(ApiDoc::openapi())
}

#[tokio::main]
async fn main() {
    let socket_address: SocketAddr = "127.0.0.1:8080".parse().unwrap();
    let listener = tokio::net::TcpListener::bind(socket_address).await.unwrap();

    let app = axum::Router::new().route("/api-docs/openapi.json", get(openapi));

    axum::serve(listener, app.into_make_service())
        .await
        .unwrap()
}
```

Let’s break this down. We have the following:

- An `ApiDoc` struct that takes the `OpenApi` derive macro and sets all the attributes required to serve the OpenAPI specification from your API.
- We have an attribute macro above our function handler. This macro will document the given information about a handler endpoint and show it in the OpenAPI spec when we open it.
- We have a “list” of responses in the macro. Note that because we have no exact type to give to OpenAPI, we leave the body as `()`.

Interested in checking out what attributes the `utoipa::path` macro can take? Have a look [here.](https://docs.rs/utoipa/latest/utoipa/attr.path.html)

If you run this code and visit `[localhost:8080/api-docs/openapi.json](http://localhost:8080/api-docs/openapi.json)` you should see a JSON response of the API specification.

This is typically good enough for just providing a basic representation. However, for internal exploration you may want to add a GUI to your OpenAPI specification. You can do this by installing the `utoipa_swagger_ui` crate and changing your `Router` to the following:

```rust
let app = Router::new().merge(
    SwaggerUi::new("/swagger-ui").url("/api-docs/openapi.json", ApiDoc::openapi())
);
```

If you run your code again and go to `localhost:8080/swagger-ui`, you’ll get a Swagger UI menu that should look something like this:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9z032lq0psc6jqjplc0k.png)

Two small things to note here are that `utoipa-service` is taken from the crate name and `crate` is the module where our route comes from. Here because our function is taken from the top level, it says `crate` - but we can fix this later on if we wanted by putting the route in a module.

If we then expand this section by clicking on the route, it will allow us to then execute the endpoint! Pretty helpful, huh?

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/z3wtgtpcod1o8fo3o7z8.png)

The `utoipa` crate is quite comprehensive, so you can be assured that it will support mostly everything you want to do. If you’d like to check out their documentation, you can do so [here.](https://docs.rs/utoipa/latest/utoipa/)

When it comes to having an API with hundreds or thousands of endpoints though, this may not be entirely ergonomic. You’ll be spending quite a lot of time writing macros which can bloat your files. To that end, you can also use the [utoipauto](https://github.com/ProbablyClem/utoipauto) crate which lets you automate all of the work with only one macro. However, this adds additional compilation time. Whether you’ll want to use it depends on your use case.

### poem-openapi

Should you be happening to use the [Poem](https://crates.io/crates/poem) framework (which hit 3.0.0 recently!), you can also use the `poem-openapi` crate to add OpenAPI functionality to your Poem service. Similarly to the `utoipa` crate, `poem-openapi` also uses macros to get OpenAPI documentation.

```rust
use poem::{listener::TcpListener, Route};
use poem_openapi::{param::Query, payload::PlainText, OpenApi, OpenApiService};

struct Api;

#[OpenApi]
impl Api {
    #[oai(path = "/hello", method = "get")]
    async fn index(&self, name: Query<Option<String>>) -> PlainText<String> {
        match name.0 {
            Some(name) => PlainText(format!("hello, {}!", name)),
            None => PlainText("hello!".to_string()),
        }
    }
}

#[tokio::main]
async fn main() -> Result<(), std::io::Error> {
    let api_service =
        OpenApiService::new(Api, "Hello World", "1.0").server("http://localhost:3000/api");
    let ui = api_service.swagger_ui();
    let app = Route::new().nest("/api", api_service).nest("/", ui);

    poem::Server::new(TcpListener::bind("0.0.0.0:3000"))
        .run(app)
        .await
}
```

Running this code will also generate a Swagger UI GUI as above, but at a different endpoint (`localhost:3000/api`).

## Generating Rust from OpenAPI specifications

Now let’s talk about generating Rust code from OpenAPI specifications. The OpenAPI collective have made a tool to generate a server client library - Rust support included! We’ll be using `npm` to install the OpenAPI generator. You can install it with the following shell snippet:

```bash
npm install @openapitools/openapi-generator-cli -g
```

There are also alternative ways to install the OpenAPI generator, which you can check out [here.](https://openapi-generator.tech/docs/installation/)

Next, we’ll need a specification to generate a client library from. The generator takes YAML or JSON files as input.  Thankfully for us, we already have a JSON filefrom the OpenAPI service we just built. If we head to `localhost:8080/api-docs/openapi.json`,  we can select the Raw Text option, prettify it and then put it all into a JSON file for input. Our file name will be `utoipa-client.json`.

Next, we’ll actually generate the client code. This can be done with the following shell snippet:

```bash
npx @openapitools/openapi-generator-cli generate -i utoipa-client.json -g rust -o ./utoipa-client
```

This looks like quite a long command! What’s happening here?

- The `-i` flag is our input file
- The `-g` flag is for the generator we should use (in this case, the `rust` one). Generators are not necessarily one-per-language, hence the flag convention.
- The `-o` flag is for the output directory. If the directory doesn’t exist, the generator will attempt to create it.

Once done, you should see a new Rust crate in the automatically generated `utoipa-client` folder. At this point in time, the generator is mostly correct. However, your generated code can have syntactical errors if your OpenAPI specification input is malformed. You can find out more about this [here.](https://docs.rs/openapi_lib_generator/latest/openapi_lib_generator/)

Interested in further customization? You can check out more OpenAPI generator configuration options [here.](https://openapi-generator.tech/docs/generators/rust)

## Working with OpenAPI specifications directly in Rust

Interested in building tools that use the OpenAPI specification? There’s a crate for that! Using the `openapiv3` crate, you can also deserialize (and serialize) to and from the OpenAPI spec format. Below, we deserialize it from a JSON string - you would additionally need `serde_json` installed:

```rust
use serde_json;
use openapiv3::OpenAPI;

fn main() {
    let data = include_str!("openapi.json");
    let openapi: OpenAPI = serde_json::from_str(data)
        .expect("Could not deserialize input");
    println!("{:?}", openapi);
}
```

There are several crates for handling this; notably, the `openapiv3` crate only supports the V3 specification. For V3.1 you want to use the `oas3` crate, which can take both YAML and JSON:

```rust
fn main() {
    match oas3::from_path("path/to/openapi.yaml") {
      Ok(spec) => println!("spec: {:?}", spec),
      Err(err) => println!("error: {}", err)
    }
}
```

## Finishing up

Thanks for reading! With this article, you should be able to tackle OpenAPI with Rust no problem.

Read more:

- [Get started with Axum](https://www.shuttle.rs/blog/2023/12/06/using-axum-rust)
- [Learn about sending your logs to Datadog](https://www.shuttle.rs/blog/2024/03/27/datadog-rust)
- [Learn about tools to help your Rust productivity](https://www.shuttle.rs/blog/2024/02/15/best-rust-tooling)