endpoints 0.3.0

Overview

Typically, a project is broken down into several sub-projects:

The endpoints sub-project contains the description of the communication protocol. The server sub-project implements this communication protocol. The client sub-project uses the protocol to communicate with the server.

Description of the HTTP endpoints

Let’s define a first artifact, cross-compiled for Scala.js, and containing a description of the endpoints of a web service.

import endpoints.algebra.{Endpoints, CirceEntities}
import io.circe.generic.JsonCodec

/**
  * Defines the HTTP endpoints description of a web service implementing a counter.
  * This web service has two endpoints: one for getting the current value of the counter,
  * and one for incrementing it.
  * It uses circe.io for JSON marshalling.
  */
trait CounterEndpoints extends Endpoints with CirceEntities {

  /**
    * Get the counter current value.
    * Uses the HTTP verb “GET” and URL path “/current-value”.
    * The response entity is a JSON document representing the counter value.
    */
  val currentValue = endpoint(get(path / "current-value"), jsonResponse[Counter])

  /**
    * Increments the counter value.
    * Uses the HTTP verb “POST” and URL path “/increment”.
    * The request entity is a JSON document representing the increment to apply to the counter.
    * The response entity is empty.
    */
  val increment = endpoint(post(path / "increment", jsonRequest[Increment]), emptyResponse)

}

@JsonCodec
case class Counter(value: Int)

@JsonCodec
case class Increment(step: Int)
CounterEndpoints.scala

JavaScript (Scala.js) client

The following code, located in the client sub-project, defines a Scala.js client for the web service.

import endpoints.xhr

/**
  * Defines an HTTP client for the endpoints described in the `CounterEndpoints` trait.
  * The derived HTTP client uses XMLHttpRequest to perform requests and returns
  * results in a `js.Thenable`.
  */
object CounterClient
  extends CounterEndpoints
    with xhr.thenable.Endpoints
    with xhr.CirceEntities
CounterClient.scala

And then, this client can be used as follows:

import scala.scalajs.js

/**
  * Performs an XMLHttpRequest on the `currentValue` endpoint, and then
  * deserializes the JSON response as a `Counter`.
  */
val eventuallyCounter: js.Thenable[Counter] = CounterClient.currentValue(())
Usage.scala

And also:

/**
  * Serializes the `Increment` value into JSON and performs an XMLHttpRequest
  * on the `increment` endpoint.
  */
val eventuallyDone: js.Thenable[Unit] = CounterClient.increment(Increment(42))
Usage.scala

Service implementation (backed by Play framework)

The following code, located in the server sub-project, defines the implementation of the web service.

import endpoints.play.server
import scala.concurrent.stm.Ref

/**
  * Defines a Play router (and reverse router) for the endpoints described in the `CounterAlg` trait.
  */
object CounterServer
  extends CounterEndpoints
    with server.Endpoints
    with server.CirceEntities {

  /** Simple implementation of an in-memory counter */
  val counter = Ref(0)

  val routes = routesFromEndpoints(

    /** Implements the `currentValue` endpoint */
    currentValue.implementedBy(_ => Counter(counter.single.get)),

    /** Implements the `increment` endpoint */
    increment.implementedBy(inc => counter.single += inc.step)

  )

}
CounterServer.scala

The CounterServer.routes value is just a play.api.routing.Router.Routes. To get an executable Web server we need to setup a “main” like the following:

import play.core.server.NettyServer

object Main extends App {
  NettyServer.fromRouter()(CounterServer.routes)
}
Main.scala

What else?

You can also get a Scala/JVM client (which uses play-ws under the hood) as follows:

import endpoints.play.client
import play.api.libs.ws.WSClient
import scala.concurrent.ExecutionContext

class CounterClient(wsClient: WSClient)(implicit ec: ExecutionContext)
  extends client.Endpoints("http://my-counter.com", wsClient)
    with CounterEndpoints
    with client.CirceEntities
CounterClient.scala

Thus, you can distribute a (fully working) JVM client, which is independent of your implementation.