Tapir là viết tắt của Typed API descRiptions (Mô tả API được định kiểu). Đây là một thư viện Scala cho phép bạn mô tả một endpoint HTTP dưới dạng một giá trị đơn giản: phương thức, đường dẫn, đầu vào, đầu ra và các trường hợp lỗi đều được nắm bắt trong hệ thống kiểu. Từ mô tả duy nhất đó, Tapir tạo ra tuyến máy chủ (server route), một client và một tài liệu OpenAPI. Bạn viết hợp đồng một lần và phần còn lại sẽ theo đó.
Đây là điều làm cho Tapir khác biệt so với việc viết tuyến trực tiếp trong một framework web. Một trình xử lý tuyến ghép nối các dây dẫn HTTP với logic. Một endpoint của Tapir tách rời chúng: mô tả là một giá trị, logic nghiệp vụ là một hàm riêng biệt và Tapir kết nối chúng. Hướng dẫn này sẽ xây dựng một API tác vụ nhỏ, kết nối nó với một máy chủ, tạo tài liệu và kiểm tra nó, với toàn bộ code Scala thực tế.
Tapir mang lại cho bạn điều gì
Tapir được xây dựng dựa trên một ý tưởng đơn giản với ba lợi ích.
Đầu tiên là an toàn kiểu. Đầu vào và đầu ra của một endpoint được định kiểu, vì vậy trình biên dịch kiểm tra xem trình xử lý của bạn có trả về đúng dạng hay không. Sự không khớp giữa những gì endpoint cam kết và những gì logic tạo ra là lỗi biên dịch, không phải lỗi sản phẩm.
Thứ hai là một nguồn chân lý duy nhất. Vì endpoint là một giá trị, bạn tạo spec OpenAPI, trình thông dịch máy chủ và một client từ cùng một mô tả. Chúng không thể bị lệch nhau, vì chỉ có một mô tả. Đây là nguyên tắc tương tự đằng sau kiểm thử hợp đồng API, được thực thi bởi trình biên dịch.
Thứ ba là độc lập framework. Mô tả endpoint không biết liệu nó chạy trên Akka HTTP, Pekko, http4s hay Netty. Bạn chọn trình thông dịch máy chủ riêng biệt, vì vậy mô tả tồn tại lâu hơn bất kỳ lựa chọn framework nào.
Cần làm rõ về những gì Tapir không phải là. Nó không phải là một framework web; nó không xử lý socket, thread pool hay công cụ định tuyến. Nó là một lớp mô tả nằm trên một framework bạn chọn. Nó cũng không phải là một trình tạo mã theo nghĩa thông thường. Bạn không chạy một công cụ viết các tệp Scala mà bạn sau đó chỉnh sửa. Các giá trị endpoint là nguồn, và máy chủ, client, và tài liệu được tính toán từ chúng tại thời điểm biên dịch. Điều đó giữ cho mô tả và mã đang chạy không bao giờ bị lệch nhau, đây là chế độ lỗi lặp đi lặp lại của các quy trình làm việc dựa trên trình tạo nơi tệp được tạo ra được chỉnh sửa thủ công và nguồn gốc bị phân kỳ.
Thiết lập dự án
Thêm Tapir vào build.sbt. Bạn cần mô-đun cốt lõi, mô-đun OpenAPI, một trình thông dịch máy chủ và một thư viện JSON. Ví dụ này sử dụng Pekko HTTP, kế nhiệm được duy trì tích cực của Akka HTTP, với circe cho 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"
)
Tài liệu Tapir hiện tại tại tapir.softwaremill.com liệt kê mọi mô-đun và các phiên bản phù hợp, vì hệ sinh thái này phát triển rất nhanh.
Định nghĩa một endpoint được định kiểu
Bắt đầu với các kiểu miền và bộ mã hóa JSON của chúng. circe suy ra các bộ mã hóa từ các lớp case.
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)
Bây giờ hãy mô tả các endpoint. Mỗi endpoint là một giá trị được xây dựng bằng cách xâu chuỗi các combinator vào cơ sở endpoint. Đọc chuỗi như một câu: đây là một GET tới /tasks/{id} nhận đầu vào đường dẫn là một số nguyên và trả về hoặc một ApiError với mã 404 hoặc một Task với mã 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]))
Bốn tham số kiểu trên PublicEndpoint là đầu vào, đầu ra lỗi, đầu ra thành công và các khả năng yêu cầu. Chưa có gì chạy ở đây. Đây là những mô tả thuần túy, đó chính xác là lý do tại sao cùng một giá trị có thể tạo ra một máy chủ, một client và tài liệu.
Một vài combinator đảm nhiệm phần lớn công việc. in thêm một đầu vào, có thể là một phân đoạn đường dẫn, một tham số truy vấn, một tiêu đề hoặc một phần thân. out mô tả phản hồi thành công. errorOut mô tả phản hồi thất bại, và một endpoint duy nhất có thể mô hình hóa một số trường hợp lỗi bằng cách kết hợp các biến thể với oneOf. Mỗi combinator làm hẹp kiểu của endpoint, vì vậy khi mô tả hoàn thành, chữ ký kiểu của nó là một tuyên bố chính xác về hợp đồng. Bạn có thể đọc chữ ký của getTask và biết nó nhận một Int, có thể thất bại với một ApiError, và nếu không thì trả về một Task, mà không cần đọc một dòng triển khai nào.
Đây là trọng tâm thực tế của Tapir. Mô tả endpoint là tài liệu mà trình biên dịch thực thi. Một đồng đội không thể đọc sai hợp đồng, bởi vì hợp đồng là một kiểu, và mã vi phạm nó sẽ không xây dựng được.
Thêm logic máy chủ
Một mô tả trở thành một tuyến hoạt động khi bạn đính kèm logic với serverLogic. Logic là một hàm trả về Either[ErrorType, SuccessType], được gói trong một kiểu hiệu ứng như Future. Một kho lưu trữ trong bộ nhớ đơn giản giữ cho ví dụ được đóng gói.
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))
}
Trình biên dịch thực thi hợp đồng ở đây. getTask cam kết một lỗi kiểu ApiError, vì vậy toRight phải cung cấp một ApiError. Trả về sai kiểu và mã sẽ không biên dịch. Các chi tiết HTTP, 404, mã hóa JSON, đã được xử lý bởi mô tả.
Chuyển đổi các server endpoint thành các tuyến thực tế với trình thông dịch Pekko và khởi động máy chủ.
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 hiện trả lời trên cổng 8080. Bạn đã thay đổi mối quan tâm về framework ở một nơi duy nhất, trình thông dịch, mà không cần chạm vào một mô tả endpoint nào.
Tạo tài liệu OpenAPI
Vì các endpoint là các giá trị, tài liệu OpenAPI là một hàm của chúng. Không có chú thích, không có tệp spec riêng để duy trì.
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 tạo ra một tài liệu OpenAPI hoàn chỉnh. Thêm mô-đun tapir-swagger-ui-bundle và Tapir sẽ phục vụ một giao diện Swagger UI tương tác trực tiếp từ máy chủ của bạn. Spec không thể nói dối về API, vì cả hai đều đến từ cùng một giá trị endpoint.
Tệp OpenAPI đó cũng là cầu nối của bạn đến các công cụ khác. Nhập nó vào Apidog và bạn sẽ có một tài liệu tham khảo API có thể duyệt, một console yêu cầu để kiểm tra thủ công và một máy chủ giả lập được tạo, tất cả mà không cần rời khỏi spec mà mã Scala của bạn đã tạo. Đó là một cách thực tế để cung cấp cho các đồng đội frontend một thứ gì đó để làm việc trong khi dịch vụ Scala đang phát triển.
Có một quy trình làm việc đáng chú ý ở đây. Tapir tạo ra spec từ mã nguồn Scala của bạn, vì vậy spec luôn được cập nhật. Một công cụ giả lập tiêu thụ spec đó sau đó cung cấp cho frontend của bạn một endpoint hoạt động trước khi máy chủ thực sự được triển khai ở bất cứ đâu. Hợp đồng chảy theo một hướng: các kiểu Scala định nghĩa nó, OpenAPI mang nó, và mock và frontend tiêu thụ nó. Không ai viết lại hợp đồng hai lần bằng tay, vì vậy không ai có thể làm cho hai bản sao bị lệch. Đối với các đội Scala cung cấp API mà các đội khác phụ thuộc vào, đây là lý do chính để giữ việc xuất OpenAPI được kết nối vào bản dựng.
Kiểm tra API
Các endpoint của Tapir có thể kiểm tra ở hai cấp độ.
Cấp độ đầu tiên kiểm tra logic của endpoint mà không cần HTTP chút nào, sử dụng trình thông dịch stub. Bạn xây dựng một backend kiểm thử từ các server endpoint và gửi yêu cầu thông qua một client sttp. Điều này nhanh vì không có gì ràng buộc một cổng.
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 }
}
}
Cấp độ thứ hai là kiểm thử tích hợp đầy đủ: ràng buộc máy chủ thực với một cổng và gọi nó bằng một client HTTP. Sử dụng điều này một cách hạn chế để xác nhận kết nối, và dựa vào các kiểm thử dựa trên stub cho phần lớn phạm vi kiểm thử của bạn vì chúng chạy nhanh hơn nhiều. Sự phân chia giữa các kiểm tra cô lập nhanh và một lớp mỏng các kiểm tra thực tế phản ánh thực hành tốt trên kiểm thử tự động nói chung.
Một thuộc tính hữu ích phát sinh từ thiết kế của Tapir: bởi vì mô tả endpoint được chia sẻ, một kiểm thử xây dựng yêu cầu từ getTask không thể gửi một yêu cầu mà máy chủ sản xuất sẽ từ chối. Hợp đồng được thực thi trên cả hai phía bởi cùng một giá trị. Để quay vòng hợp đồng đó thông qua khám phá thủ công và một máy chủ giả lập, hãy Tải Apidog và nhập tệp OpenAPI mà Tapir tạo ra.
Các câu hỏi thường gặp
Tapir trong Scala là gì?
Tapir, viết tắt của Typed API descRiptions, là một thư viện Scala để mô tả các endpoint HTTP dưới dạng các giá trị được định kiểu. Từ một mô tả, nó tạo ra tuyến máy chủ, một client và một tài liệu OpenAPI, do đó hợp đồng được định nghĩa một lần và duy trì nhất quán trên cả ba.
Tôi có phải sử dụng Akka HTTP với Tapir không?
Không. Tapir tách mô tả endpoint khỏi trình thông dịch máy chủ. Bạn có thể chạy cùng các endpoint trên Pekko HTTP, http4s, Netty, Vert.x hoặc các framework khác bằng cách chọn một mô-đun trình thông dịch khác. Các mô tả không thay đổi.
Tapir tạo tài liệu OpenAPI như thế nào?
Mô-đun OpenAPI của Tapir đọc các giá trị endpoint của bạn và tạo ra một tài liệu OpenAPI hoàn chỉnh từ chúng. Vì tài liệu và máy chủ đều xuất phát từ cùng một mô tả, tài liệu không thể bị lệch khỏi API thực tế.
Làm cách nào để kiểm thử một API Tapir mà không cần khởi động máy chủ?
Sử dụng trình thông dịch stub của Tapir. Nó xây dựng một backend kiểm thử sttp từ các server endpoint của bạn, vì vậy bạn có thể gửi yêu cầu và xác nhận phản hồi mà không cần ràng buộc một cổng. Các kiểm thử này nhanh, và bạn giữ một số lượng nhỏ các kiểm thử tích hợp đầy đủ để xác nhận kết nối thực.
Tôi có thể sử dụng đầu ra của Tapir với các công cụ API khác không?
Có. Tài liệu OpenAPI mà Tapir tạo ra là một spec tiêu chuẩn. Bạn có thể nhập nó vào các công cụ như Apidog để có một tài liệu tham khảo API tương tác, một console yêu cầu và một máy chủ giả lập được tạo, điều này hữu ích cho việc chia sẻ API với các nhà phát triển frontend.
