Cara Membuat dan Menguji HTTP API dengan Tapir di Scala

INEZA Felin-Michel

INEZA Felin-Michel

22 May 2026

Cara Membuat dan Menguji HTTP API dengan Tapir di Scala

Apidog untuk Perusahaan

Penerapan On-Premises

SSO & RBAC

Sesuai SOC 2

Jelajahi Apidog Enterprise

Tapir adalah singkatan dari Typed API descRiptions. Ini adalah pustaka Scala yang memungkinkan Anda mendeskripsikan titik akhir HTTP sebagai nilai biasa: metode, jalur, input, output, dan kasus kesalahannya semuanya ditangkap dalam sistem tipe. Dari satu deskripsi tersebut, Tapir memperoleh rute server, klien, dan dokumen OpenAPI. Anda menulis kontrak sekali dan sisanya mengikuti.

Inilah yang membuat Tapir berbeda dari menulis rute langsung di kerangka kerja web. Penangan rute menggabungkan pengkabelan HTTP dengan logika. Titik akhir Tapir memisahkannya: deskripsinya adalah satu nilai, logika bisnis adalah fungsi terpisah, dan Tapir menggabungkannya. Tutorial ini membangun API tugas kecil, menghubungkannya ke server, menghasilkan dokumen, dan mengujinya, dengan Scala asli di seluruhnya.

Apa yang Tapir Berikan kepada Anda

Tapir dibangun di atas ide sederhana dengan tiga manfaat.

Yang pertama adalah keamanan tipe. Input dan output titik akhir diketik, sehingga kompilator memeriksa bahwa penangan Anda mengembalikan bentuk yang benar. Ketidakcocokan antara apa yang dijanjikan titik akhir dan apa yang dihasilkan logika adalah kesalahan kompilasi, bukan bug produksi.

Yang kedua adalah satu sumber kebenaran. Karena titik akhir adalah suatu nilai, Anda menghasilkan spesifikasi OpenAPI, interpreter server, dan klien dari deskripsi yang sama. Keduanya tidak dapat menyimpang, karena hanya ada satu deskripsi. Ini adalah disiplin yang sama di balik pengujian kontrak API, yang diterapkan oleh kompilator.

Yang ketiga adalah kemandirian kerangka kerja. Deskripsi titik akhir tidak mengetahui apakah ia berjalan di Akka HTTP, Pekko, http4s, atau Netty. Anda memilih interpreter server secara terpisah, sehingga deskripsi tersebut bertahan lebih lama dari pilihan kerangka kerja apa pun.

Penting untuk dijelaskan apa yang bukan Tapir. Ini bukan kerangka kerja web; ia tidak menangani soket, thread pool, atau mesin perutean. Ini adalah lapisan deskripsi yang berada di atas kerangka kerja yang Anda pilih. Ini juga bukan generator kode dalam arti biasa. Anda tidak menjalankan alat yang menulis file Scala yang kemudian Anda edit. Nilai titik akhir adalah sumbernya, dan server, klien, serta dokumen dihitung dari nilai tersebut pada waktu kompilasi. Itu menjaga agar deskripsi dan kode yang berjalan tidak pernah ketinggalan zaman, yang merupakan mode kegagalan berulang dari alur kerja berbasis generator di mana file yang dihasilkan diedit secara manual dan sumbernya menyimpang.

Menyiapkan Proyek

Tambahkan Tapir ke build.sbt. Anda memerlukan modul inti, modul OpenAPI, interpreter server, dan pustaka JSON. Contoh ini menggunakan Pekko HTTP, penerus Akka HTTP yang aktif dipelihara, dengan circe untuk JSON.

val tapirVersion = "1.11.7"

libraryDependencies ++= Seq(
  "com.softwaremill.sttp.tapir" %% "tapir-core" % tapirVersion,
  "com.softwaremill.sttp.tapir" %% "tapir-pekko-http-server" % tapirVersion,
  "com.softwaremill.sttp.tapir" %% "tapir-json-circe" % tapirVersion,
  "com.softwaremill.sttp.tapir" %% "tapir-openapi-docs" % tapirVersion,
  "com.softwaremill.sttp.apispec" %% "openapi-circe-yaml" % "0.11.3"
)

Dokumentasi Tapir saat ini di tapir.softwaremill.com mencantumkan setiap modul dan versi yang cocok, karena ekosistem bergerak cepat.

Mendefinisikan Titik Akhir Bertipe

Mulai dengan tipe domain dan codec JSON-nya. circe menurunkan codec dari kelas kasus.

import io.circe.generic.auto._
import sttp.tapir._
import sttp.tapir.json.circe._
import sttp.tapir.generic.auto._

case class Task(id: Int, title: String, done: Boolean)
case class NewTask(title: String)
case class ApiError(message: String)

Sekarang deskripsikan titik-titik akhir. Masing-masing adalah nilai yang dibangun dengan merantai kombinator ke dasar endpoint. Baca rantai tersebut sebagai sebuah kalimat: ini adalah GET ke /tasks/{id} yang mengambil input jalur integer dan mengembalikan salah satu ApiError dengan 404 atau Task dengan 200.

val getTask: PublicEndpoint[Int, ApiError, Task, Any] =
  endpoint.get
    .in("tasks" / path[Int]("id"))
    .errorOut(statusCode(sttp.model.StatusCode.NotFound).and(jsonBody[ApiError]))
    .out(jsonBody[Task])

val listTasks: PublicEndpoint[Unit, Unit, List[Task], Any] =
  endpoint.get
    .in("tasks")
    .out(jsonBody[List[Task]])

val createTask: PublicEndpoint[NewTask, Unit, Task, Any] =
  endpoint.post
    .in("tasks")
    .in(jsonBody[NewTask])
    .out(statusCode(sttp.model.StatusCode.Created).and(jsonBody[Task]))

Empat parameter tipe pada PublicEndpoint adalah input, output kesalahan, output keberhasilan, dan kemampuan yang diperlukan. Belum ada yang berjalan di sini. Ini adalah deskripsi murni, itulah sebabnya nilai yang sama dapat menghasilkan server, klien, dan dokumentasi.

Beberapa kombinator memiliki bobot paling besar. in menambahkan input, apakah itu segmen jalur, parameter kueri, header, atau badan. out mendeskripsikan respons keberhasilan. errorOut mendeskripsikan respons kegagalan, dan satu titik akhir dapat memodelkan beberapa kasus kesalahan dengan menggabungkan varian dengan oneOf. Setiap kombinator mempersempit tipe titik akhir, jadi pada saat deskripsi selesai, tanda tangan tipenya adalah pernyataan yang tepat dari kontrak. Anda dapat membaca tanda tangan getTask dan mengetahui bahwa ia mengambil Int, dapat gagal dengan ApiError, dan sebaliknya mengembalikan Task, tanpa membaca satu baris implementasi pun.

Ini adalah inti praktis dari Tapir. Deskripsi titik akhir adalah dokumentasi yang diterapkan oleh kompilator. Rekan tim tidak dapat salah membaca kontrak, karena kontrak adalah sebuah tipe, dan kode yang melanggarnya tidak akan terkompilasi.

Menambahkan Logika Server

Deskripsi menjadi rute yang berfungsi ketika Anda melampirkan logika dengan serverLogic. Logika adalah fungsi yang mengembalikan Either[ErrorType, SuccessType], dibungkus dalam tipe efek seperti Future. Penyimpanan dalam memori sederhana menjaga contoh tetap mandiri.

import scala.concurrent.Future
import scala.collection.concurrent.TrieMap
import sttp.tapir.server.ServerEndpoint

val store = TrieMap[Int, Task](
  1 -> Task(1, "Write the Tapir tutorial", done = false)
)

val getTaskServer: ServerEndpoint[Any, Future] =
  getTask.serverLogic { id =>
    Future.successful(
      store.get(id).toRight(ApiError(s"No task with id $id"))
    )
  }

val listTasksServer: ServerEndpoint[Any, Future] =
  listTasks.serverLogic(_ => Future.successful(Right(store.values.toList)))

val createTaskServer: ServerEndpoint[Any, Future] =
  createTask.serverLogic { newTask =>
    val id = store.size + 1
    val task = Task(id, newTask.title, done = false)
    store.put(id, task)
    Future.successful(Right(task))
  }

Kompilator menerapkan kontrak di sini. getTask menjanjikan kesalahan bertipe ApiError, jadi toRight harus menyediakan ApiError. Kembalikan tipe yang salah dan kode tidak akan terkompilasi. Detail HTTP, 404, pengodean JSON, sudah ditangani oleh deskripsi.

Konversi titik akhir server ke rute aktual dengan interpreter Pekko dan mulai server.

import sttp.tapir.server.pekkohttp.PekkoHttpServerInterpreter
import org.apache.pekko.http.scaladsl.Http
import org.apache.pekko.actor.typed.ActorSystem
import org.apache.pekko.actor.typed.scaladsl.Behaviors

implicit val system: ActorSystem[Any] = ActorSystem(Behaviors.empty, "task-api")
import system.executionContext

val routes = PekkoHttpServerInterpreter().toRoute(
  List(getTaskServer, listTasksServer, createTaskServer)
)

Http().newServerAt("localhost", 8080).bind(routes)

API sekarang menjawab di port 8080. Anda mengubah masalah kerangka kerja di satu tempat, interpreter, tanpa menyentuh satu pun deskripsi titik akhir.

Menghasilkan Dokumentasi OpenAPI

Karena titik-titik akhir adalah nilai, dokumen OpenAPI adalah fungsi dari nilai tersebut. Tanpa anotasi, tanpa file spesifikasi terpisah untuk dipelihara.

import sttp.tapir.docs.openapi.OpenAPIDocsInterpreter
import sttp.apispec.openapi.circe.yaml._

val docs = OpenAPIDocsInterpreter().toOpenAPI(
  List(getTask, listTasks, createTask),
  "Task API",
  "1.0"
)

println(docs.toYaml)

docs.toYaml menghasilkan dokumen OpenAPI yang lengkap. Tambahkan modul tapir-swagger-ui-bundle dan Tapir menyajikan UI Swagger interaktif langsung dari server Anda. Spesifikasi tidak dapat berbohong tentang API, karena keduanya berasal dari nilai titik akhir yang sama.

File OpenAPI itu juga merupakan jembatan Anda ke alat lain. Impor ke Apidog dan Anda mendapatkan referensi API yang dapat dijelajahi, konsol permintaan untuk pemeriksaan manual, dan server tiruan yang dihasilkan, semuanya tanpa meninggalkan spesifikasi yang sudah dihasilkan oleh kode Scala Anda. Ini adalah cara praktis untuk memberikan sesuatu kepada rekan tim frontend untuk dikerjakan sementara layanan Scala berkembang.

Ada alur kerja yang perlu disebutkan di sini. Tapir menghasilkan spesifikasi dari sumber Scala Anda, jadi spesifikasi selalu terkini. Alat mocking yang mengkonsumsi spesifikasi tersebut kemudian memberikan titik akhir yang berfungsi kepada frontend Anda sebelum server nyata diterapkan di mana pun. Kontrak mengalir dalam satu arah: tipe Scala mendefinisikannya, OpenAPI membawanya, dan mock serta frontend mengkonsumsinya. Tidak ada yang menulis kontrak dua kali secara manual, sehingga tidak ada yang bisa membuat dua salinan tersebut tidak sinkron. Bagi tim Scala yang mengirimkan API yang diandalkan tim lain, inilah alasan utama untuk menjaga ekspor OpenAPI tetap terhubung ke build.

Menguji API

Titik akhir Tapir dapat diuji pada dua tingkatan.

Tingkat pertama memeriksa logika titik akhir tanpa HTTP sama sekali, menggunakan interpreter stub. Anda membangun backend uji dari titik akhir server dan mengirim permintaan melalui klien sttp. Ini cepat karena tidak ada yang mengikat port.

import org.scalatest.flatspec.AsyncFlatSpec
import org.scalatest.matchers.should.Matchers
import sttp.client3._
import sttp.client3.testing.SttpBackendStub
import sttp.tapir.server.stub.TapirStubInterpreter
import sttp.client3.circe._
import io.circe.generic.auto._

class TaskApiSpec extends AsyncFlatSpec with Matchers {

  val backend = TapirStubInterpreter(SttpBackendStub.asynchronousFuture)
    .whenServerEndpointRunLogic(getTaskServer)
    .whenServerEndpointRunLogic(createTaskServer)
    .backend()

  "GET /tasks/1" should "return the seeded task" in {
    basicRequest
      .get(uri"http://test.com/tasks/1")
      .response(asJson[Task])
      .send(backend)
      .map { resp =>
        resp.code.code shouldBe 200
        resp.body.map(_.title) shouldBe Right("Write the Tapir tutorial")
      }
  }

  "GET /tasks/999" should "return a 404 with an error body" in {
    basicRequest
      .get(uri"http://test.com/tasks/999")
      .response(asJson[ApiError])
      .send(backend)
      .map { resp => resp.code.code shouldBe 404 }
  }
}

Tingkat kedua adalah pengujian integrasi penuh: ikat server nyata ke port dan panggil dengan klien HTTP. Gunakan ini secukupnya untuk mengkonfirmasi pengkabelan, dan andalkan pengujian berbasis stub untuk sebagian besar cakupan Anda karena mereka berjalan jauh lebih cepat. Pemisahan antara pemeriksaan terisolasi cepat dan lapisan tipis yang nyata mencerminkan praktik baik di seluruh pengujian otomatis secara umum.

Properti yang berguna muncul dari desain Tapir: karena deskripsi titik akhir dibagikan, pengujian yang membangun permintaan dari getTask tidak dapat mengirim permintaan yang akan ditolak oleh server produksi. Kontrak diterapkan di kedua sisi oleh nilai yang sama. Untuk menguji kontrak itu melalui eksplorasi manual dan server tiruan, Unduh Apidog dan impor file OpenAPI yang dihasilkan Tapir.

Pertanyaan yang Sering Diajukan

Apa itu Tapir di Scala?

Tapir, singkatan dari Typed API descRiptions, adalah pustaka Scala untuk mendeskripsikan titik akhir HTTP sebagai nilai bertipe. Dari satu deskripsi, ia menurunkan rute server, klien, dan dokumen OpenAPI, sehingga kontrak didefinisikan sekali dan tetap konsisten di ketiganya.

Apakah saya harus menggunakan Akka HTTP dengan Tapir?

Tidak. Tapir memisahkan deskripsi titik akhir dari interpreter server. Anda dapat menjalankan titik akhir yang sama di Pekko HTTP, http4s, Netty, Vert.x, atau lainnya dengan memilih modul interpreter yang berbeda. Deskripsi tidak berubah.

Bagaimana Tapir menghasilkan dokumentasi OpenAPI?

Modul OpenAPI Tapir membaca nilai titik akhir Anda dan menghasilkan dokumen OpenAPI lengkap darinya. Karena dokumen dan server keduanya berasal dari deskripsi yang sama, dokumentasi tidak dapat ketinggalan zaman dengan API yang sebenarnya.

Bagaimana cara menguji API Tapir tanpa memulai server?

Gunakan interpreter stub Tapir. Ini membangun backend uji sttp dari titik akhir server Anda, sehingga Anda dapat mengirim permintaan dan menegaskan respons tanpa mengikat port. Pengujian ini cepat, dan Anda mempertahankan sejumlah kecil pengujian integrasi penuh untuk mengkonfirmasi pengkabelan nyata.

Bisakah saya menggunakan output Tapir dengan alat API lainnya?

Ya. Dokumen OpenAPI yang dihasilkan Tapir adalah spesifikasi standar. Anda dapat mengimpornya ke alat seperti Apidog untuk mendapatkan referensi API interaktif, konsol permintaan, dan server tiruan yang dihasilkan, yang berguna untuk berbagi API dengan pengembang frontend.

Mengembangkan API dengan Apidog

Apidog adalah alat pengembangan API yang membantu Anda mengembangkan API dengan lebih mudah dan efisien.