spray

REST on Akka

Typesafe webinar 2013-11-19

Mathias Doenitz   / /

Slides: http://spray.io/webinar/
Video recording: http://spray.io/webinar/video/

What is spray?

  • embeddable HTTP stack for your
    Akka (Scala) applications
  • focus: HTTP integration layers
    rather than web applications
  • server- and client-side

But, why?

Isn't HTTP on the JVM a "solved" problem?

Can't we just use Netty?
(or Servlets, or Restlet, or Undertow, ...)

Yes, we can

(it's being done all the time)


But: Do we want to?

Not really!

  • servlet containers?
  • XML configuration?
  • mutable data models / APIs?
  • Java Collections?
  • adapter layers?
  • limited type-safety?

What we want

  • `case class`-based model
  • actor-based APIs (message protocols)
  • functions as values
  • Scala/Akka Futures
  • Scala collections
  • type classes
  • type safety

What we also want

We want to build on Akka!

  • our whole application,
    not just a few parts!
  • same principles, concepts and coding
    style in all layers of the stack:
    much easier problem analysis & tuning

spray builds on Akka

  • entirely built in Scala, no wrapping of Java libraries
  • fully async and non-blocking
  • only one type of active components in all layers: actors
  • core API style: message protocol
  • actor-friendly (e.g. "tell don't ask")
  • fast, lightweight, modular, testable

spray components

spray components spray components spray components spray components spray components

plus: spray-servlet, spray-testkit, ...

HTTP model

  • `case class`-based data model
  • high-level abstractions for most things HTTP
  • fully immutable, little logic
  • predefined instances for common media types, status codes, encodings, charsets, cache-control directives, etc.
  • open for extension
    (e.g. registration of custom media types)

HTTP model: show me code


case class HttpRequest(
  method: HttpMethod = HttpMethods.GET,
  uri: Uri = Uri./,
  headers: List[HttpHeader] = Nil,
  entity: HttpEntity = HttpEntity.Empty,
  protocol: HttpProtocol = HttpProtocols.`HTTP/1.1`
) extends HttpMessage
					

HTTP model: show me code


case class HttpResponse(
  status: StatusCode = StatusCodes.OK,
  entity: HttpEntity = HttpEntity.Empty,
  headers: List[HttpHeader] = Nil,
  protocol: HttpProtocol = HttpProtocols.`HTTP/1.1`
) extends HttpMessage
					

HTTP model: show me code


case class Uri(             // proper RFC 3986
  scheme: String,           // compliant,
  authority: Authority,     // immutable
  path: Path,               // URI model
  query: Query,             // with a fast,
  fragment: Option[String]) // custom parser
					

HTTP model: show me code


case class `Accept-Charset`(charsetRanges: Seq[HttpCharsetRange])
  extends HttpHeader

case class `Accept-Encoding`(encodings: Seq[HttpEncodingRange])
  extends HttpHeader

case class `Set-Cookie`(cookie: HttpCookie)
  extends HttpHeader

case class RawHeader(name: String, value: String)
  extends HttpHeader
					

Low-level HTTP layer

  • directly sits on top of the new Akka IO
  • performs TCP HTTP "translation"
  • cleanly separated layer of actors
    provided as an Akka Extension
  • implements "essentials",
    no higher-level features (like file serving)

The IO stack

IO stack

Towards Akka IO (briefly)

  • network communication is
  • packet-based
  • no continuous flow of bytes
  • rather: chunked into messages

Towards Akka IO (2)

  • old-school Java IO (before NIO): stream-based
  • input: read a stream,
    block if no data
  • output: write to stream,
    block if sending is not currently possible
  •   paradigm mismatch: stream-based vs. message based

Towards Akka IO (3)

  • Java NIO ("new" IO):
  • extended API with support for
    async, non-blocking IO ops
  • but:
    • hard to use
    • still not message-based

Akka IO

  • bridges the gap between Java NIO and Akka actors
  • msg-based API surfaces the nature of the network
Akka IO Akka IO Akka IO Akka IO Akka IO

spray-can

  • provides message-based APIs on multiple levels
    (server-side: connection-level,
    client-side: connection-, host- and request-level)
  • maximum throughput with acceptable latency
  • massive numbers of concurrent connections
  • HTTP pipelining
  • chunked messages (streaming)
  • SSL/TLS encryption

spray-can: show me code


class PingPongService extends Actor {
  def receive = {
    // when a new connection comes in we register
    // ourselves as the connection handler
    case _: Http.Connected ⇒ sender ! Http.Register(self)

    // can you guess what this does?
    case HttpRequest(GET, Uri.Path("/"), _, _, _) ⇒
      sender ! HttpResponse(entity = "PONG")
  }
}
					

spray-routing

  • internal DSL for the direct interface layer to the application
  • type-safe, yet flexible (thanks to shapeless)
  • much more than just routing: behavior definition
  • small and simple building blocks: directives
  • highly composable

API Layer: How it fits in

application API layer API layer API layer API layer API layer API layer

API Layer Responsibilities

  • request routing based on method, path, query, entity
  • (Un)marshalling to / from domain objects
  • encoding / decoding (compression)
  • authentication / authorization
  • caching and serving static content
  • RESTful error handling

API Layer: show me code


class MyServiceActor extends HttpServiceActor {
  def receive = runRoute {
    path("order" / HexIntNumber) { id =>
      get {
        complete {
          "Received GET request for order " + id
        }
      } ~
      put {
        complete {
          "Received PUT request for order " + id
        }
      }
    }
  }
}
				

Predefined Directives (RC3)

alwaysCache, anyParam, anyParams, authenticate, authorize, autoChunk, cache, cachingProhibited, cancelAllRejections, cancelRejection, clientIP, complete, compressResponse, compressResponseIfRequested, cookie, decodeRequest, decompressRequest, delete, deleteCookie, detach, dynamic, dynamicIf, encodeResponse, entity, extract, failWith, formField, formFields, get, getFromBrowseableDirectories, getFromBrowseableDirectory, getFromDirectory, getFromFile, getFromResource, getFromResourceDirectory, handleExceptions, handleRejections, handleWith, head, headerValue, headerValueByName, headerValuePF, hextract, host, hostName, hprovide, jsonpWithParameter, listDirectoryContents, logRequest, logRequestResponse, logResponse, mapHttpResponse, mapHttpResponseEntity, mapHttpResponseHeaders, mapHttpResponsePart, mapInnerRoute, mapRejections, mapRequest, mapRequestContext, mapRouteResponse, mapRouteResponsePF, method, noop, onComplete, onFailure, onSuccess, optionalCookie, optionalHeaderValue, optionalHeaderValueByName, optionalHeaderValuePF, options, overrideMethodWithParameter, parameter, parameterMap, parameterMultiMap, parameters, parameterSeq, pass, patch, path, pathPrefix, pathPrefixTest, pathSuffix, pathSuffixTest, post, produce, provide, put, rawPath, rawPathPrefix, rawPathPrefixTest, redirect, reject, rejectEmptyResponse, requestEncodedWith, requestEntityEmpty, requestEntityPresent, respondWithHeader, respondWithHeaders, respondWithLastModifiedHeader, respondWithMediaType, respondWithSingletonHeader, respondWithSingletonHeaders, respondWithStatus, responseEncodingAccepted, rewriteUnmatchedPath, routeRouteResponse, scheme, schemeName, setCookie, unmatchedPath, validate

Real-World Example


lazy val route = {
  encodeResponse(Gzip) {
    pathSingleSlash {
      get {
        redirect("/doc")
      }
    } ~
    pathPrefix("api") {
      jsonpWithParameter("callback") {
        path("top-articles") {
          get {
            parameter("max".as[Int]) { max =>
              validate(max >= 0, "query parameter 'max' must be >= 0") {
                complete {
                  (topArticlesService ? max).mapTo[Seq[Article]]
                }
              }
            }
          }
        } ~
        tokenAuthenticate { user =>
          path("ranking") {
            get {
              countAndTime(user, "ranking") {
                parameters("fixed" ? 0, "mobile" ? 0, "sms" ? 0, "mms" ? 0,
                           "data" ? 0).as(RankingDescriptor) { descr =>
                  complete {
                    (rankingService ? Ranking(descr)).mapTo[RankingResult]
                  }
                }
              }
            }
          } ~
          path("accounts") {
            post {
              authorize(user.isAdmin) {
                content(as[AccountDetails]) { details =>
                  complete {
                    (accountService ? NewAccount(details)).mapTo[OpResult]
                  }
                }
              }
            }
          } ~
          path("account" / IntNumber) { accountId =>
            get { ... } ~
            put { ... } ~
            delete { ... }
          }
        }
      } ~
      pathPrefix("v1") {
        proxyToDjango
      }
    } ~
    pathPrefix("doc") {
      respondWithHeader(`Cache-Control`(`max-age`(3600))) {
        transformResponse(_.withContentTransformed(markdown2Html)) {
          getFromResourceDirectory("doc/root",
                                   pathRewriter = appendFileExt)
        }
      }
    } ~
  } ~
  cacheIfEnabled {
    encodeResponse(Gzip) {
      getFromResourceDirectory("public")
    }
  }
}
				

Best Practices

  • Don't block in your routes!
  • Keep route structure clean and readable,

    pull out all logic into custom directives
  • Don’t let API layer leak into application
  • Use (Un)marshalling infrastructure
  • Use sbt-revolver for fast dev turn-around

There is more...

What's next?

  • spray 1.0 / 1.1 / 1.2 final
  • afterwards: spray becomes akka-http
  • Play will gradually move onto akka-http
  • Improvements, features, rounding off
    • websockets
    • SPDY
    • ...

Getting started

THANK YOU!

Q & A