🐫
Scala, Akka HTTP, CalibanでGraphQL APIを実装する
この記事は
ScalaのGraphQLライブラリといえばSangria
がより有名だが
-
ZIO
との連携の容易さ - Schema周りのboilerplate codeの少なさ
からCaliban
を試すことにしたので導入のメモを残しておきます
公式のドキュメント
Authorのblog を参考にした今回作成したものは
Calibanについて
純粋な関数型ライブラリでデータ型からGraphQLスキーマを自動的に導き出すMagnolia、クエリを解析するFastparse、様々な効果を処理するZIOに依存している。
依存の追加
Caliban
を使うためにbuild.sbt
に依存を追加
libraryDependencies += "com.github.ghostdogpr" %% "caliban" % "0.9.5"
HTTP ServerはAkka Http
, JsonはCirce
を使うため"caliban-akka-http"
を選択した
libraryDependencies += "com.github.ghostdogpr" %% "caliban-akka-http" % "0.9.5"
libraryDependencies += "de.heikoseeberger" %% "akka-http-circe" % "1.35.3"
あとAkka Http
のServerを立ち上げるのにAkkaのActorを使いますが, 今からやるならTyped
が良さそうなので依存に追加してます
libraryDependencies += "com.typesafe.akka" %% "akka-actor-typed" % "2.6.12"
Schema定義
以下のGraphQLを実装するとします
type Query {
users: [User!]!
user(id: Int): User
}
type User {
id: Int!
name: String!
}
Schemaを用意して
case class UserSchema(id: Int, name: String)
(今回は手動で用意しましたが, 既にSchemaがある場合に自動生成もできるようで良さそうです)
Query定義
Queries
を作成します
API(GraphQLのQuery)を表すQueries
という名前のケースクラスを作成し、公開したい関数にちなんだ名前とモデルを持つ2つのフィールドを作成します。次に、実際の関数を呼び出すこのクラスの値を作成します。
object ExampleApi extends GenericSchema[GetUserService] {
case class Queries(
users: URIO[GetUserService, Seq[UserSchema]],
user: UserIdArgs => URIO[GetUserService, Option[UserSchema]]
)
GraphQL APIの定義を作成します。まず、RootResolverの中にQueryResolverを入れます。RootResolverは、query, mustation, subscribeを含むルートオブジェクトです。Queryだけは必須です。
val api: GraphQL[Console with Clock with GetUserService] =
graphQL(
RootResolver(
Queries(
GetUserService.findUsers,
args => GetUserService.findUser(args.id)
)
)
)
}
Queriesで定義しといたGetUserService
を用意する
object GetUserService {
type GetUserService = Has[Service]
trait Service {
def findUsers: UIO[Seq[UserSchema]]
def findByUserId(id: Int): UIO[Option[UserSchema]]
}
def findUsers: URIO[GetUserService, Seq[UserSchema]] = URIO.accessM(_.get.findUsers)
def findUser(id: Int): URIO[GetUserService, Option[UserSchema]] = URIO.accessM(_.get.findByUserId(id))
def make(initial: Seq[UserSchema] = sample): ZLayer[Any, Nothing, GetUserService] = ZLayer.fromEffect {
for {
users <- Ref.make(initial)
} yield new Service {
def findUsers: UIO[Seq[UserSchema]] = users.get
def findByUserId(id: Int): UIO[Option[UserSchema]] = users.get.map(_.find(c => c.id == id))
}
}
}
Http Server
Akka Http Serverに
- ZIOのRuntime
- Route
を設定すれば完成です
object ExampleApp extends App with AkkaHttpCirceAdapter {
implicit val system: ActorSystem[String] = ActorSystem(Behaviors.empty[String], "example-app")
implicit val executionContext: ExecutionContextExecutor = system.executionContext
implicit val runtime: Runtime[GetUserService with Console with Clock] =
Runtime.unsafeFromLayer(GetUserService.make() ++ Console.live ++ Clock.live, Platform.default)
val interpreter = runtime.unsafeRun(ExampleApi.api.interpreter)
val route = path("graphql") {
adapter.makeHttpService(interpreter)
}
val bindingFuture = Http().newServerAt("localhost", 8088).bind(route)
sys.addShutdownHook {
bindingFuture
.flatMap(_.unbind())
.onComplete(_ => system.terminate())
}
}
これでlocalからqueryを実行できるようになりました
❯ curl -X POST \
http://localhost:8088/graphql \
-H 'Host: localhost:8088' \
-H 'Content-Type: application/json' \
-d '{
"query": "query { users { id name }}"
}'
{
"data" : {
"users" : [
{
"id" : 1,
"name" : "name1"
},
{
"id" : 2,
"name" : "name2"
}
]
}
}
Discussion