Dokumentasi API yang tertinggal dari kode adalah dokumen yang berbohong. OpenAPI (sebelumnya Swagger) adalah standar industri untuk mendeskripsikan REST API secara mesin-readable. Library springdoc-openapi memindai @RestController Spring dan menghasilkan dokumen OpenAPI otomatis. Pada chapter ini kita akan memahami struktur OpenAPI dan memodelkan registry endpoint yang mencetak JSON OpenAPI.
OpenAPI 3.0 #
OpenAPI 3.0 adalah spesifikasi JSON/YAML untuk mendeskripsikan REST API. Struktur utamanya:
{
"openapi": "3.0.3",
"info": { "title": "...", "version": "..." },
"tags": [ { "name": "...", "description": "..." } ],
"paths": {
"/users": {
"get": { "summary": "...", "parameters": [...], "responses": {...} },
"post": { ... }
}
}
}Field paths memetakan URL ke operasi HTTP. Setiap operasi memiliki summary, parameters, dan responses. Klien dapat memparse dokumen ini untuk generate SDK, mock server, atau dokumentasi interaktif.
springdoc-openapi #
springdoc-openapi adalah library Spring Boot yang memindai classpath untuk @RestController, membaca anotasi @GetMapping/@PostMapping, dan menghasilkan JSON di endpoint /v3/api-docs. Library juga menyediakan Swagger UI di /swagger-ui.html — antarmuka interaktif untuk mencoba API.
// build.gradle.kts
implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.6.0")
// Controller dengan anotasi OpenAPI
@Tag(name = "users", description = "Manajemen pengguna")
@RestController
@RequestMapping("/users")
class UserController {
@Operation(summary = "Daftar pengguna", description = "Mengembalikan seluruh pengguna")
@ApiResponse(responseCode = "200", description = "OK")
@GetMapping
fun list(): List<User> = ...
}Anotasi @Tag, @Operation, @Parameter, @ApiResponse berasal dari OpenAPI. Springdoc membaca anotasi ini dan menggabungkannya dengan refleksi Spring MVC untuk menghasilkan dokumen lengkap.
Anatomi Endpoint OpenAPI #
Setiap endpoint dijelaskan oleh beberapa field:
| Field | Peran |
|---|---|
summary | Judul singkat operasi (1 baris) |
description | Penjelasan detail (paragraf) |
parameters | Daftar parameter (path, query, header) |
responses | Map status code ke deskripsi |
tags | Pengelompokan logis |
deprecated | Tandai operasi sebagai deprecated |
Parameter memiliki in (path, query, header, cookie), required, description, dan schema. Responses memiliki code (status HTTP) dan description.
Demo OpenAPI Registry #
Karena springdoc-openapi nyata membutuhkan Spring Boot runtime, kita akan memodelkan registry endpoint sebagai class biasa. Program ini mendaftar lima endpoint, menghasilkan JSON OpenAPI 3.0, dan mensimulasikan Swagger UI route.
// api_documentation.kt
// Mendemonstrasikan auto-generate dokumentasi API OpenAPI/Swagger.
// Pada kode produksi, springdoc-openapi memindai @RestController dan
// menghasilkan /v3/api-docs serta UI Swagger. Di sini kita memodelkannya
// sebagai registry yang mengumpulkan endpoint metadata dan mencetak
// OpenAPI JSON.
// Mock anotasi OpenAPI
@Target(AnnotationTarget.CLASS) annotation class Tag(val name: String, val description: String = "")
@Target(AnnotationTarget.FUNCTION) annotation class Operation(
val summary: String = "",
val description: String = "",
val deprecated: Boolean = false
)
@Target(AnnotationTarget.VALUE_PARAMETER) annotation class Parameter(
val name: String,
val `in`: String = "path",
val required: Boolean = true,
val description: String = ""
)
@Target(AnnotationTarget.VALUE_PARAMETER) annotation class ApiResponse(
val code: Int,
val description: String
)
data class EndpointInfo(
val method: String,
val path: String,
val summary: String,
val description: String,
val deprecated: Boolean,
val tags: List<String>,
val parameters: List<ParamInfo>,
val responses: Map<Int, String>
)
data class ParamInfo(val name: String, val `in`: String, val required: Boolean, val description: String)
// OpenAPI Registry: mengumpulkan endpoint yang dipindai dari controller
class OpenApiRegistry {
private val endpoints = mutableListOf<EndpointInfo>()
private val tags = mutableMapOf<String, String>()
fun addTag(name: String, description: String) { tags[name] = description }
fun addEndpoint(info: EndpointInfo) { endpoints.add(info) }
fun endpoints(): List<EndpointInfo> = endpoints.toList()
// Generate JSON OpenAPI 3.0
fun toJson(): String {
val sb = StringBuilder()
sb.append("{\n")
sb.append(""" "openapi": "3.0.3",""").append("\n")
sb.append(""" "info": {"title": "Demo API", "version": "1.0.0"},""").append("\n")
val tagsJson = tags.entries.joinToString(",") { (n, d) ->
"""{"name":"$n","description":"$d"}"""
}
sb.append(""" "tags": [$tagsJson],""").append("\n")
sb.append(""" "paths": {""")
val paths = endpoints.groupBy { it.path }
val pathStrs = paths.map { (path, infos) ->
val methods = infos.joinToString(",") { e ->
val m = e.method.lowercase()
val params = e.parameters.joinToString(",") { p ->
"""{"name":"${p.name}","in":"${p.`in`}","required":${p.required},"description":"${p.description}","schema":{"type":"string"}}"""
}
val responses = e.responses.entries.joinToString(",") { (code, desc) ->
""""$code":{"description":"$desc"}"""
}
val deprecation = if (e.deprecated) """"deprecated": true,""" else ""
val tagsArr = e.tags.joinToString(",") { """"$it"""" }
""""$m":{"summary":"${e.summary}","description":"${e.description}",$deprecation"parameters":[$params],"responses":{$responses},"tags":[$tagsArr]}"""
}
""""$path":{$methods}"""
}
sb.append(pathStrs.joinToString(","))
sb.append("}\n")
sb.append("}")
return sb.toString()
}
}
// Mock controller yang akan dipindai
@Tag(name = "users", description = "Manajemen pengguna")
class UserController
@Tag(name = "health", description = "Health check")
class HealthController
fun registerEndpoints(reg: OpenApiRegistry) {
reg.addEndpoint(EndpointInfo(
method = "GET",
path = "/users",
summary = "Daftar semua pengguna",
description = "Mengembalikan array berisi seluruh pengguna terdaftar.",
deprecated = false,
tags = listOf("users"),
parameters = emptyList(),
responses = mapOf(200 to "OK", 500 to "Server error")
))
reg.addEndpoint(EndpointInfo(
method = "GET",
path = "/users/{id}",
summary = "Detail pengguna",
description = "Mengembalikan satu pengguna berdasarkan id.",
deprecated = false,
tags = listOf("users"),
parameters = listOf(ParamInfo("id", "path", true, "ID pengguna")),
responses = mapOf(200 to "OK", 404 to "Pengguna tidak ditemukan")
))
reg.addEndpoint(EndpointInfo(
method = "POST",
path = "/users",
summary = "Buat pengguna baru",
description = "Membuat pengguna baru. Body berisi nama dan email.",
deprecated = false,
tags = listOf("users"),
parameters = listOf(ParamInfo("body", "body", true, "Data pengguna baru")),
responses = mapOf(201 to "Created", 400 to "Input invalid")
))
reg.addEndpoint(EndpointInfo(
method = "DELETE",
path = "/users/{id}",
summary = "Hapus pengguna (deprecated)",
description = "Penghapusan di-deprecate; gunakan soft delete via PATCH.",
deprecated = true,
tags = listOf("users"),
parameters = listOf(ParamInfo("id", "path", true, "ID pengguna")),
responses = mapOf(204 to "No Content", 404 to "Not found")
))
reg.addEndpoint(EndpointInfo(
method = "GET",
path = "/health",
summary = "Status server",
description = "Endpoint publik untuk health check.",
deprecated = false,
tags = listOf("health"),
parameters = emptyList(),
responses = mapOf(200 to "OK")
))
}
fun main() {
val reg = OpenApiRegistry()
registerEndpoints(reg)
println("=== Endpoint terdaftar (${reg.endpoints().size}) ===")
reg.endpoints().forEach { e ->
val deprec = if (e.deprecated) " [DEPRECATED]" else ""
println(" ${e.method.padEnd(7)} ${e.path}$deprec")
println(" summary: ${e.summary}")
println(" tags: ${e.tags}, params: ${e.parameters.size}, responses: ${e.responses.keys}")
}
println("\n=== OpenAPI 3.0 JSON (ringkas) ===")
println(reg.toJson())
println("\n=== Swagger UI route (di Spring Boot otomatis tersedia di /swagger-ui.html) ===")
println(" GET /swagger-ui.html -> UI interaktif")
println(" GET /v3/api-docs -> JSON di atas")
}Output yang Dihasilkan:
=== Endpoint terdaftar (5) ===
GET /users
summary: Daftar semua pengguna
tags: [users], params: 0, responses: [200, 500]
GET /users/{id}
summary: Detail pengguna
tags: [users], params: 1, responses: [200, 404]
POST /users
summary: Buat pengguna baru
tags: [users], params: 1, responses: [201, 400]
DELETE /users/{id} [DEPRECATED]
summary: Hapus pengguna (deprecated)
tags: [users], params: 1, responses: [204, 404]
GET /health
summary: Status server
tags: [health], params: 0, responses: [200]
=== OpenAPI 3.0 JSON (ringkas) ===
{
"openapi": "3.0.3",
"info": {"title": "Demo API", "version": "1.0.0"},
"tags": [],
"paths": {"/users":{"get":{"summary":"Daftar semua pengguna","description":"Mengembalikan array berisi seluruh pengguna terdaftar.","parameters":[],"responses":{"200":{"description":"OK"},"500":{"description":"Server error"}},"tags":["users"]},"post":{"summary":"Buat pengguna baru","description":"Membuat pengguna baru. Body berisi nama dan email.","parameters":[{"name":"body","in":"body","required":true,"description":"Data pengguna baru","schema":{"type":"string"}}],"responses":{"201":{"description":"Created"},"400":{"description":"Input invalid"}},"tags":["users"]}},"/users/{id}":{"get":{"summary":"Detail pengguna","description":"Mengembalikan satu pengguna berdasarkan id.","parameters":[{"name":"id","in":"path","required":true,"description":"ID pengguna","schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"404":{"description":"Pengguna tidak ditemukan"}},"tags":["users"]},"delete":{"summary":"Hapus pengguna (deprecated)","description":"Penghapusan di-deprecate; gunakan soft delete via PATCH.","deprecated": true,"parameters":[{"name":"id","in":"path","required":true,"description":"ID pengguna","schema":{"type":"string"}}],"responses":{"204":{"description":"No Content"},"404":{"description":"Not found"}},"tags":["users"]}},"/health":{"get":{"summary":"Status server","description":"Endpoint publik untuk health check.","parameters":[],"responses":{"200":{"description":"OK"}},"tags":["health"]}}}
}
=== Swagger UI route (di Spring Boot otomatis tersedia di /swagger-ui.html) ===
GET /swagger-ui.html -> UI interaktif
GET /v3/api-docs -> JSON di atasMari kita bedah bagian penting kode di atas:
data class EndpointInfo(val method, val path, val summary, ...): model satu endpoint OpenAPI. Fielddeprecatedmenandai endpoint yang akan dihapus; parameter dan responses dijelaskan per endpoint.class OpenApiRegistry { private val endpoints = mutableListOf<EndpointInfo>() }: registry yang menyimpan endpoint. Pada produksi, springdoc mengisi registry otomatis via refleksi — kita tidak perlu memanggiladdEndpointmanual.val paths = endpoints.groupBy { it.path }: kelompokkan endpoint berdasarkan path. OpenAPI menyatakan bahwa satu path dapat memiliki multiple method (GET, POST, DELETE di URL yang sama).val deprecation = if (e.deprecated) """"deprecated": true,""" else "": bila endpoint deprecated, sertakan fielddeprecated: truedi JSON. Swagger UI akan menandai endpoint ini dengan label deprecated.fun toJson(): String: generator JSON manual. Pada produksi, library sepertikotlinx.serializationatau Jackson menangani ini — tetapi struktur output sama.
Swagger UI #
Swagger UI adalah antarmuka HTML yang membaca JSON OpenAPI dan menampilkan dokumentasi interaktif. Setiap endpoint dapat dieksekusi langsung dari browser — pengguna mengisi parameter, klik “Try it out”, dan melihat respons nyata. Swagger UI sangat berguna untuk onboarding klien API baru.
Referensi Lanjutan #
- Ktor routing — panduan routing Ktor sebagai sumber kebenaran struktur endpoint yang akan didokumentasikan
- Spring guides — kumpulan panduan Spring untuk mendokumentasikan REST API
Selanjutnya #
Setelah dokumentasi siap, langkah berikutnya adalah tugas latar belakang. Chapter Background Jobs akan membahas @Scheduled, scheduler Quartz, dan cron expression untuk tugas yang berjalan teratur.