Mengotomasi dokumentasi API: generate spesifikasi OpenAPI dari kode dengan utoipa, menghidupkan Swagger UI dan ReDoc, serta menjaga dokumentasi tetap sinkron dengan implementasi lewat attribute annotations.

API yang baik butuh dokumentasi — dan dokumentasi yang ditulis tangan hampir selalu ketinggalan zaman. Episode ini membawa solusi khas Rust: generate dokumentasi dari kode. Dengan utoipa, spesifikasi OpenAPI (dulu Swagger) dihasilkan dari struct dan handler kalian secara otomatis, lalu dihidupkan sebagai Swagger UI dan ReDoc.
Mengapa OpenAPI penting? Karena ia menjadi contract yang bisa dibaca mesin dan manusia sekaligus: tim frontend membaca endpoint, tooling menghasilkan klien SDK, dan test dapat memvalidasi. Dengan utoipa, dokumentasi tidak bisa ketinggalan dari kode — keduanya berjalan beriringan.
cargo add utoipa --features axum_extras,chrono,uuid
cargo add utoipa-swagger-ui --features axumCatatan fitur:
axum_extras — integrasi utoipa dengan extractor Axum (Path, Query, Json).chrono + uuid — dukungan serialisasi tipe-tipe yang kita pakai.utoipa-swagger-ui — menyediakan SwaggerUi service untuk routing.Langkah pertama: beri tahu utoipa bentuk skema data:
use serde::{Deserialize, Serialize};
use utoipa::ToSchema;
#[derive(Debug, Serialize, ToSchema)]
pub struct Post {
pub id: uuid::Uuid,
#[schema(example = "Hello Axum")]
pub title: String,
pub body: String,
pub published: bool,
pub created_at: chrono::DateTime<chrono::Utc>,
pub updated_at: chrono::DateTime<chrono::Utc>,
}
#[derive(Debug, Deserialize, ToSchema)]
pub struct NewPost {
#[schema(min_length = 3, max_length = 120, example = "Hello Axum")]
pub title: String,
pub body: String,
pub published: Option<bool>,
}#[derive(ToSchema)] mengubah struct menjadi bagian skema OpenAPI. Attribute #[schema(...)] menambahkan metadata — contoh nilai, batas panjang — yang muncul di dokumentasi.
Note
Struct yang sama dipakai di response dan request (Post vs NewPost) sengaja dipisah: ini membuat skema OpenAPI lebih akurat — field yang tidak boleh dikirim klien (misal id dan created_at) tidak muncul di skema request.
Handler dianotasi dengan #[utoipa::path(...)] yang menjelaskan route, method, parameter, dan response:
use utoipa::OpenApi;
#[utoipa::path(
get,
path = "/posts",
tag = "posts",
responses(
(status = 200, description = "Daftar post berhasil", body = [Post]),
(status = 500, description = "Error internal")
)
)]
pub async fn list_posts(State(state): State<AppState>) -> AppResult<Json<Vec<Post>>> {
// ... implementasi dari episode 9
}
#[utoipa::path(
post,
path = "/posts",
tag = "posts",
request_body = NewPost,
responses(
(status = 201, description = "Post dibuat", body = Post),
(status = 400, description = "Body tidak valid")
)
)]
pub async fn create_post(State(state): State<AppState>, Json(body): Json<NewPost>) -> AppResult<(StatusCode, Json<Post>)> {
// ...
}Attribute utama yang perlu dipahami:
| Attribute | Arti |
|---|---|
get/post/patch/delete | HTTP method |
path | URL endpoint (harus sama persis dengan route) |
tag | Pengelompokan endpoint di UI (misal posts, auth) |
responses(...) | Status code + deskripsi + body skema |
request_body | Skema body untuk method yang memakai JSON |
Kumpulkan semua endpoint ke dalam satu struct OpenApi:
use utoipa::OpenApi;
#[derive(OpenApi)]
#[openapi(
paths(
list_posts,
create_post,
get_post,
update_post,
delete_post,
crate::handlers::auth::login
),
components(schemas(Post, NewPost, UpdatePost, LoginRequest)),
tags(
(name = "posts", description = "CRUD artikel"),
(name = "auth", description = "Autentikasi")
)
)]
pub struct ApiDoc;Lalu pasang Swagger UI ke router:
use utoipa::OpenApi;
use utoipa_swagger_ui::SwaggerUi;
fn app() -> Router {
Router::new()
.merge(SwaggerUi::new("/docs")
.url("/api-docs/openapi.json", ApiDoc::openapi()))
.merge(posts_routes())
}Sekarang buka http://localhost:3000/docs — Swagger UI menampilkan semua endpoint, skema, dan tombol "Try it out" untuk eksekusi langsung. File spesifikasi mentah tersedia di /api-docs/openapi.json — bisa diunduh dan diimport ke tool lain (Postman, Stoplight, generator SDK).
ReDoc adalah alternatif UI yang lebih cocok untuk dibaca sebagai dokumentasi referensi (layout tiga kolom). Utoipa menyediakan keduanya:
use utoipa_swagger_ui::{SwaggerUi, Url};
let swagger_ui = SwaggerUi::new("/docs")
.url("/api-docs/openapi.json", ApiDoc::openapi())
.urls(vec![
Url::new("API v1", "/api-docs/openapi.json"),
]);Untuk ReDoc, pass sebagai route terpisah memakai utoipa-redoc crate, atau cukup andalkan openapi.json yang bisa di-paste ke https://redocly.github.io/redoc/. Untuk series ini, Swagger UI sudah memadai; ReDoc menjadi nilai tambah untuk dokumentasi publik yang indah.
Skema Path dan Query yang dipakai handler otomatis dideteksi utoipa jika memakai tipe Deserialize yang ToSchema. Contoh query dengan parameter terdokumentasi:
use utoipa::{IntoParams, ToSchema};
#[derive(serde::Deserialize, IntoParams)]
pub struct Pagination {
#[param(minimum = 1, default = 1)]
pub page: Option<u32>,
#[param(minimum = 1, maximum = 100, default = 20)]
pub per_page: Option<u32>,
}
#[utoipa::path(
get,
path = "/posts",
tag = "posts",
params(Pagination),
responses((status = 200, description = "Daftar post", body = [Post]))
)]
pub async fn list_posts(
State(state): State<AppState>,
Query(p): Query<Pagination>,
) -> AppResult<Json<Vec<Post>>> {
// ...
}Dengan IntoParams, parameter query muncul di dokumentasi dengan batasan minimum/maksimum — klien tahu batas sebelum memanggil.
Warning
#[utoipa::path] hanya menampilkan apa yang kalian tulis — path di attribute harus cocok dengan route di Router. Jika salah, dokumentasi menampilkan endpoint yang tidak ada (atau sebaliknya). Tambahkan test snapshot di episode berikutnya (atau test yang memvalidasi setiap path) agar mismatch terdeteksi di CI.
Untuk endpoint yang butuh Authorization: Bearer, tandai di attribute:
#[utoipa::path(
get,
path = "/me",
tag = "auth",
security(
("bearer_auth" = [])
),
responses(
(status = 200, description = "Profil user", body = PublicUser),
(status = 401, description = "Token tidak valid")
)
)]
pub async fn profile(Extension(claims): Extension<Claims>) -> AppResult<Json<PublicUser>> {
// ...
}Dan deklarasikan skema security di ApiDoc:
#[openapi(
paths(profile, login),
components(schemas(PublicUser, LoginRequest)),
modifiers(&SecurityAddon)
)]
struct SecurityAddon;
impl utoipa::Modify for SecurityAddon {
fn modify(&self, openapi: &mut utoipa::openapi::OpenApi) {
if let Some(components) = openapi.components.as_mut() {
components.add_security_scheme(
"bearer_auth",
utoipa::openapi::security::HttpAuthScheme::Bearer,
);
}
}
}Hasilnya: tombol "Authorize" di Swagger UI, dan UI otomatis menambahkan header Authorization ke setiap request yang ditandai bearer_auth. Pengujian API dari dokumentasi jadi satu langkah.
Pada episode 15 ini API kalian punya dokumentasi hidup:
ToSchema untuk model, #[utoipa::path] untuk endpoint.ApiDoc merakit semua path + schemas + tags./docs, spesifikasi mentah di /api-docs/openapi.json.IntoParams.security("bearer_auth") + tombol Authorize.Di episode 16 selanjutnya kita bereskan configuration & environment: memindahkan semua nilai berubah (DATABASE_URL, JWT secret, port) ke env vars dan config, plus graceful shutdown dengan signal handling. Sampai jumpa di episode 16!