This crate adds OpenAPI support to Loco by using a initializer.
The Loco OpenAPI integration is generated using Utoipa
Edit your Cargo.toml
file
Add the loco-openapi
initializer, with one or multiple of the following features flags:
swagger
redoc
scalar
full
# Cargo.toml
[dependencies]
loco-openapi = { version = "*", features = [
"full",
], git = "https://github.com/loco-rs/loco-openapi-Initializer", branch = "main" }
Add the corresponding OpenAPI visualizer to the config file
# config/*.yaml
#...
initializers:
openapi:
redoc:
url: /redoc
# spec_json_url: /redoc/openapi.json
# spec_yaml_url: /redoc/openapi.yaml
scalar:
url: /scalar
# spec_json_url: /scalar/openapi.json
# spec_yaml_url: /scalar/openapi.yaml
swagger:
url: /swagger
spec_json_url: /api-docs/openapi.json # spec_json_url is required for swagger-ui
# spec_yaml_url: /api-docs/openapi.yaml
In the initializer you can modify the OpenAPI spec before the routes are added, allowing you to edit openapi::info
// src/app.rs
use loco_openapi::prelude::*;
async fn initializers(ctx: &AppContext) -> Result<Vec<Box<dyn Initializer>>> {
Ok(vec![Box::new(
loco_openapi::OpenapiInitializerWithSetup::new(
|ctx| {
#[derive(OpenApi)]
#[openapi(
modifiers(&SecurityAddon),
info(
title = "Loco Demo",
description = "This app is a kitchensink for various capabilities and examples of the [Loco](https://loco.rs) project."
)
)]
struct ApiDoc;
set_jwt_location_ctx(ctx);
ApiDoc::openapi()
},
// When using automatic schema collection only
None,
// When using manual schema collection
// Manual schema collection can also be used at the same time as automatic schema collection
// Some(vec![controllers::album::api_routes()]),
),
)])
}
Only routes that are annotated with utoipa::path
will be included in the OpenAPI spec.
use loco_openapi::prelude::*;
/// Your Title here
///
/// Your Description here
#[utoipa::path(
get,
path = "/album",
responses(
(status = 200, description = "Album found", body = Album),
),
)]
async fn get_action_openapi() -> Result<Response> {
format::json(Album {
title: "VH II".to_string(),
rating: 10,
})
}
Make sure to add #[derive(ToSchema)]
on any struct that included in utoipa::path
.
use loco_openapi::prelude::*;
#[derive(Serialize, Debug, ToSchema)]
pub struct Album {
title: String,
rating: u32,
}
Swap the axum::routing::MethodRouter
to openapi(MethodRouter<AppContext>, UtoipaMethodRouter<AppContext>)
+ use loco_openapi::prelude::*;
Routes::new()
- .add("/album", get(get_album)),
+ .add("/get_album", openapi(get(get_album), routes!(get_album))),
In the initializer, if you are only using automatic schema collection, set the second arg to None
, to disable manual schema collection
use loco_openapi::prelude::*;
async fn initializers(ctx: &AppContext) -> Result<Vec<Box<dyn Initializer>>> {
Ok(vec![Box::new(
loco_openapi::OpenapiInitializerWithSetup::new(
|ctx| {
// ...
},
None,
),
)])
}
Create a function that returns OpenApiRouter<AppContext>
use loco_openapi::prelude::*;
pub fn routes() -> Routes {
Routes::new()
.prefix("api/album/")
.add("/get_album", get(get_album))
}
pub fn api_routes() -> OpenApiRouter<AppContext> {
OpenApiRouter::new().routes(routes!(get_album))
}
Then in the initializer, create a Vec<OpenApiRouter<AppContext>>
use loco_openapi::prelude::*;
async fn initializers(ctx: &AppContext) -> Result<Vec<Box<dyn Initializer>>> {
Ok(vec![Box::new(
loco_openapi::OpenapiInitializerWithSetup::new(
|ctx| {
// ...
},
Some(vec![controllers::album::api_routes()]),
),
)])
}
It's possible to use both manual and automatic schema collection at the same time. But make sure to only add each route once.
Using both of the examples above, at the same time will not work, since the route /get_album
will be added twice.
// controllers
use loco_openapi::prelude::*;
pub fn routes() -> Routes {
Routes::new()
.prefix("api/album/")
// automatic schema collection of `/get_album` is here
.add("/get_album", openapi(get(get_album), routes!(get_album))),
}
pub fn api_routes() -> OpenApiRouter<AppContext> {
// OpenApiRouter for manual schema collection of `/get_album` is created here
OpenApiRouter::new().routes(routes!(get_album))
}
// initializers
use loco_openapi::prelude::*;
async fn initializers(ctx: &AppContext) -> Result<Vec<Box<dyn Initializer>>> {
Ok(vec![Box::new(
loco_openapi::OpenapiInitializerWithSetup::new(
|ctx| {
// ...
},
// manual schema collection is added to the OpenAPI spec here
Some(vec![controllers::album::api_routes()]),
),
)])
}
routes!(get_action_1_do_not_do_this, get_action_2_do_not_do_this))
If modifiers(&SecurityAddon)
is set in inital_openapi_spec
, you can document the per route security in utoipa::path
:
security(("jwt_token" = []))
security(("api_key" = []))
- or leave blank to remove security from the route
security(())
Example:
use loco_openapi::prelude::*;
#[utoipa::path(
get,
path = "/album",
security(("jwt_token" = [])),
responses(
(status = 200, description = "Album found", body = Album),
),
)]
After running cargo loco start
the OpenAPI visualizers are available at the following URLs by default:
To customize the OpenAPI visualizers URLs,and endpoint paths for json and yaml, see config/*.yaml
.
Because of global shared state issues when using automatic schema collection, it's recommended to disable the loco-openapi-initializer
when running tests in your application.
async fn initializers(ctx: &AppContext) -> Result<Vec<Box<dyn Initializer>>> {
let mut initializers: Vec<Box<dyn Initializer>> = vec![];
if ctx.environment != Environment::Test {
initializers.push(
Box::new(
loco_openapi::OpenapiInitializerWithSetup::new(
|ctx| {
// ...
},
None,
),
) as Box<dyn Initializer>
);
}
Ok(initializers)
}
Alternatively you could use (cargo nextest
)[https://nexte.st/]. This issue is not relevant when using the loco-openapi-initializer
for normal use.