Skip to content

prayagupa/scala-rest-frameworks

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

13 Commits
 
 
 
 

Repository files navigation

scala REST libraries comparison

scalatra

1) setup/ bootstrap

  • install sbt
  • add scalatra dependency
  • add scalatra-json dependency
  • add json4s dependency

2) Server backend (to implement HTTP requests and HTTP responses)

Jetty - Eclipse Jetty provides a Web server and javax.servlet container, plus support for HTTP/2, WebSocket etc

3) REST API definition/ Routes

  • provides http methods as function. request handler is bit weird as it returns Any instead of just T. meaning single resource can respond different types. also it should be action: HttpRequest => [T]
def get(transformers: RouteTransformer*)(action: => Any): Route = addRoute(Get, transformers, action)
  • has to read headers/cookies/ request payload etc explicitly inside a action function

example,

import org.scalatra.{AsyncResult, FutureSupport, ScalatraServlet}
import scala.concurrent.{ExecutionContext, Future}
import org.json4s.{DefaultFormats, Formats}
import org.scalatra.json.JacksonJsonSupport
import org.scalatra.{AsyncResult, FutureSupport, ScalatraServlet}
import scala.concurrent.ExecutionContext.Implicits

scala> final case class ApiResponse(correlationId: String, message: String)
defined class ApiResponse

scala> class RestServer extends ScalatraServlet with JacksonJsonSupport with FutureSupport {
        protected implicit val jsonFormats: Formats = DefaultFormats
      
        protected implicit def executor: ExecutionContext = Implicits.global
      
        post("/api/chat") {
          new AsyncResult() {
            override val is: Future[ApiResponse] = Future {
            
              val correlationId = request.getHeader("correlationId")
              val body = request.body
              
              ApiResponse(correlationId, "hi, how can i help ya")
            }
          }
        }
      }
defined class RestServer

4) Request validation

Scalatra includes a very sophisticated set of validation commands.

These allow you to parse incoming data, instantiate command objects, and automatically apply validations to the objects.

http://scalatra.org/guides/2.3/formats/commands.html

5) REST Error Handling

provides generic error handler for response 500 as a partial function

  final case class ApiError(errorCode: String, errorMessage: String)
  
  error {
    case e: Exception => ApiError("InternalApiError", e.getMessage)
  }
  

6) Request Deserialization/ Response Serialisation

Uses json4s which default encoders and decoders

http://scalatra.org/guides/2.3/formats/json.html

7) Exposing API definition

No a clean way to expose API definition to consumers as headers/ cookies/ req payload are not part of API definition

8) API http-client

9) Performance

jetty perf - 182,147 req/sec

10) Latency

jetty - 1.2 ms for above perf

11) example

https://github.com/duwamish-os/config-management-ui

play-framework

1) setup/ bootstrap

  • install sbt
  • add plugin PlayScala
  • add play-json

2) Server backend (to implement HTTP requests and HTTP responses)

3) REST API definition/ Routes

Two places for endpoint definition. Play provides routing DSL as partial function

trait Router {
  def routes: Router.Routes
  
  //
}

object Router {
    type Routes = PartialFunction[RequestHeader, Handler]
}
  • has to read headers/cookies/ request payload etc explicitly inside a Handler function

example,

Endopint routes

import api.GameApi
import play.api.libs.json.Json
import play.api.mvc.{Action, AnyContent, RequestHeader}
import play.api.routing._
import play.api.routing.sird._
import play.api.mvc._

class ApiRouter extends SimpleRouter {

  override def routes: PartialFunction[RequestHeader, Action[AnyContent]] = {
    case GET(p"/game/$correlationId") =>
      Action {
        Results.Ok(Json.toJson(GameApi.instance.scores(correlationId)))
      }
  }
}

conf/routes

->      /api                        route.ApiRouter

API

import play.api.libs.json.{Json, OWrites, Reads}

final case class ApiResponse(correlationID: String, scores: List[Map[String, String]])

object ApiResponse {
  implicit val reads: Reads[ApiResponse] = Json.reads[ApiResponse]
  implicit val writes: OWrites[ApiResponse] = Json.writes[ApiResponse]
}

class GameApi {

  def scores(correlationId: String): ApiResponse = {
    val response = ApiResponse(correlationId, List(
      Map("player" -> "upd",
        "score" -> "1000"),

      Map("player" -> "dude",
        "score" -> "999")))

    response
  }

}

object GameApi {
  lazy val instance = new GameApi
}

4) Request validation

https://www.playframework.com/documentation/2.6.x/ScalaActionsComposition#Validating-requests

5) REST Error Handling

https://www.playframework.com/documentation/2.6.x/ScalaErrorHandling

6) Request Deserialization/ Response Serialisation

Uses play-json which demands deserializers and serializers for every type

7) Exposing API definition

No a clean way to expose API definition to consumers as headers/ cookies/ req payload are not part of API definition

8) API http-client

Provides play-ws async http clinet - https://www.playframework.com/documentation/2.6.x/ScalaWS which uses netty-client

9) Performance

93,002 req/sec

10) Latency

6.3 ms/req

11) example

https://github.com/duwamish-os/scala-play-war

finch

1) setup/ bootstrap

  • install sbt
  • add finch-core
  • add finch-circe

2) Server backend (to implement HTTP requests and HTTP responses)

Netty is a NIO client server framework which enables quick and easy development of network applications such as protocol servers and clients. It greatly simplifies and streamlines network programming such as TCP and UDP socket server.

https://blog.twitter.com/engineering/en_us/a/2014/netty-at-twitter-with-finagle.html

3) REST API definition/ Routes

provides clean routing API as a function Input => EndpointResult[T] - https://finagle.github.io/finch/user-guide.html#understanding-endpoints

private[finch] trait EndpointMappers {

  def get[A](e: Endpoint[A]): EndpointMapper[A] = new EndpointMapper[A](Method.Get, e)

  //
}

example,

Endopint routes

scala> final case class PurchaseRequest(items: List[String])
defined class PurchaseRequest

scala> final case class PurchaseResponse(correlationId: String)
defined class PurchaseResponse
import com.twitter.finagle.http.{Request, Response}
import com.twitter.finagle.{Http, Service}
import com.twitter.util.{Await, Future}
import io.circe.generic.auto._
import io.finch._
import io.finch.circe._
import io.finch.syntax._

import scala.concurrent.ExecutionContext.Implicits.global
import scala.util.{Failure, Success}
import io.finch.syntax.scalaFutures._

scala> object PurchasesServer {
      
          val chatEndpoint: Endpoint[PurchaseResponse] = post("api" :: "purchases" :: header[String]("correlationId") :: jsonBody[PurchaseRequest]) {
            (correlationId: String, purchaseRequest: PurchaseRequest) => {
      
              concurrent.Future {
                PurchaseResponse(correlationId)
              }.map(Ok)
      
            }
      
          }
            
      }
defined object PurchasesServer

4) Request validation

Provides well designed request validation API

https://finagle.github.io/finch/user-guide.html#validation

scala> val purchases = post("api" :: "purchases" :: header[Int]("purchaseId").should("be greater than length 5"){_ > 5})
purchases: io.finch.syntax.EndpointMapper[Int] = POST /api :: purchases :: header(purchaseId)

5) REST Error Handling

Provides error handler (takes partial function) for each API endpoint

https://finagle.github.io/finch/user-guide.html#errors

6) Request Deserialization/ Response Serialisation

Uses circe which provides compile time derivation of deserializers and serializers

Also provides support for other poular functional json libraries - https://finagle.github.io/finch/user-guide.html#json

7) Exposing API definition

Headers/Cookies are part of part of API definition. It helps the API definition to be exposed cleanly to the consumers as a jar dependency with small change

8) API http-client

Provides featherbed as async http-client - https://finagle.github.io/featherbed/doc/06-building-rest-clients.html

9) Performance

396,796 req/sec

10) Latency

6.3 ms

11) example

https://github.com/duwamish-os/chat-server

1) setup/ bootstrap

  • install sbt
  • add http4s-blaze-server dependency
  • add http4s-circe dependency
  • add http4s-dsl dependency

2) Server backend (to implement HTTP requests and HTTP responses)

blaze - blaze is a scala library for building async pipelines, with a focus on network IO

netty support is coming on as well - http4s/http4s#1831

3) REST API definition/ Routes

  • provides dsl for routes Http4sDsl.
object -> {
  def unapply[F[_]](req: Request[F]): Some[(Method, Path)] =
    Some((req.method, Path(req.pathInfo)))
}

case class /(parent: Path, child: String) extends Path {
  lazy val toList: List[String] = parent.toList ++ List(child)

  def lastOption: Some[String] = Some(child)

  lazy val asString: String = s"$parent/${UrlCodingUtils.pathEncode(child)}"

  override def toString: String = asString

  def startsWith(other: Path): Boolean = {
    val components = other.toList
    toList.take(components.length) === components
  }
}
  • has to read headers/cookies/ request payload etc explicitly inside a match function

example,

import cats.effect.IO
import fs2.StreamApp
import io.circe._
import io.circe.generic.auto._
import io.circe.syntax._
import org.http4s._
import org.http4s.circe._
import org.http4s.dsl.Http4sDsl
import org.http4s.server.blaze.BlazeBuilder
import schema.{ChatHistory, ChatRequest, ChatResponse}


import scala.concurrent.ExecutionContext.Implicits.global

object RestServer extends StreamApp[IO] with Http4sDsl[IO] {

  val service: HttpService[IO] = HttpService[IO] {

    case req@GET -> Root / "api" / "chat" / "history" / customerID =>
      val correlationId = req.headers.find(_.name == "correlationId").map(_.value).getOrElse("")
      Ok(Future(ChatHistory(correlationId, List("hi, how can i help you?", "here is coffee shop"))))

    case request@POST -> Root / "api" / "chat" =>
      for {
        chatRequest <- request.as[ChatRequest]
        chatResponse <- Ok(Future(ChatResponse(chatRequest.correlationId, "Here are near by coffee shops")))
      } yield chatResponse

  }
  
  ///
}

6) Request Deserialization/ Response Serialisation

uses circe so should be able to auto encode/decode. also provides jsonEncoderOf

  def jsonEncoderOf[F[_]: EntityEncoder[?[_], String]: Applicative, A](
      implicit encoder: Encoder[A]): EntityEncoder[F, A] =
    jsonEncoderWithPrinterOf(defaultPrinter)

https://http4s.org/v0.15/json/

7) Exposing API definition

Headers/Cookies are not part of part of API definition.

8) API http-client

maybe https://github.com/http4s/rho

9) Performance

82,652 req/sec

10) Latency

2.3 ms

11) example

https://github.com/duwamish-os/http4s-rest-server

Releases

No releases published

Packages

No packages published