Skip to content

Instantly share code, notes, and snippets.

@masakielastic
Last active December 26, 2025 20:46
Show Gist options
  • Select an option

  • Save masakielastic/af2e86469b390785800be54d2ace1cbf to your computer and use it in GitHub Desktop.

Select an option

Save masakielastic/af2e86469b390785800be54d2ace1cbf to your computer and use it in GitHub Desktop.
Poem で OpenAPI 対応のドキュメント管理

Poem で OpenAPI 対応のドキュメント管理

Cargo.toml を次のように書く。

[package]
name = "poem-rest"
version = "0.1.0"
edition = "2021"

[dependencies]
poem = "3"
poem-openapi = { version = "5", features = ["swagger-ui"] }

tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
thiserror = "1"
tracing-subscriber = "0.3"

src/main.rs を編集する。今回の確認作業では500エラーは使わないので Internal(Json<ErrorBody>) はコメントアウトした。

次のコマンドを実行する。

cargo run

ブラウザーで http://localhost:3000/docs を確認する。

HTTP API を確認する。

curl -i http://127.0.0.1:3000/api/health
HTTP/1.1 200 OK
content-type: application/json; charset=utf-8
content-length: 11
date: Fri, 26 Dec 2025 20:34:21 GMT
curl http://127.0.0.1:3000/api/users/1
{"id":1,"name":"Alice"}
curl http://127.0.0.1:3000/api/users/2
{"code":"NOT_FOUND","message":"not found"}
use poem::{listener::TcpListener, middleware::Tracing, Route, Server};
use poem_openapi::{
payload::Json,
param::Path,
ApiResponse, Object, OpenApi, OpenApiService,
};
use serde::{Deserialize, Serialize};
#[derive(Debug, Object, Serialize)]
struct ErrorBody {
code: String,
message: String,
}
#[derive(Debug, ApiResponse)]
enum ApiErr {
#[oai(status = 400)]
BadRequest(Json<ErrorBody>),
#[oai(status = 404)]
NotFound(Json<ErrorBody>),
// #[oai(status = 500)]
// Internal(Json<ErrorBody>),
}
fn bad_request(msg: impl Into<String>) -> ApiErr {
ApiErr::BadRequest(Json(ErrorBody { code: "BAD_REQUEST".into(), message: msg.into() }))
}
fn not_found() -> ApiErr {
ApiErr::NotFound(Json(ErrorBody { code: "NOT_FOUND".into(), message: "not found".into() }))
}
#[derive(Debug, Object, Serialize, Deserialize)]
struct Health {
ok: bool,
}
#[derive(Debug, Object, Serialize, Deserialize)]
struct User {
id: i32,
name: String,
}
#[derive(Debug, Object, Deserialize)]
struct CreateUser {
name: String,
}
struct Api;
#[OpenApi]
impl Api {
#[oai(path = "/health", method = "get")]
async fn health(&self) -> Json<Health> {
Json(Health { ok: true })
}
#[oai(path = "/users/:id", method = "get")]
async fn get_user(&self, id: Path<i32>) -> Result<Json<User>, ApiErr> {
let id = id.0;
if id == 1 {
Ok(Json(User { id, name: "Alice".into() }))
} else {
Err(not_found())
}
}
#[oai(path = "/users", method = "post")]
async fn create_user(&self, body: Json<CreateUser>) -> Result<Json<User>, ApiErr> {
let name = body.0.name.trim().to_string();
if name.is_empty() {
return Err(bad_request("name must not be empty"));
}
Ok(Json(User { id: 100, name }))
}
}
#[tokio::main]
async fn main() -> Result<(), std::io::Error> {
tracing_subscriber::fmt::init();
let api_service = OpenApiService::new(Api, "Poem REST API", "0.1.0")
.server("http://127.0.0.1:3000/api");
let ui = api_service.swagger_ui();
let app = Route::new()
.nest("/api", api_service)
.nest("/docs", ui)
.with(Tracing);
Server::new(TcpListener::bind("127.0.0.1:3000"))
.run(app)
.await
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment