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:
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:
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 theOpenApi
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.
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:
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:
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?
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.
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 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 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.
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:
npm install @openapitools/openapi-generator-cli -g
There are also alternative ways to install the OpenAPI generator, which you can check out here.
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:
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, therust
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.
Interested in further customization? You can check out more OpenAPI generator configuration options here.
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:
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:
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: