Quick start

The central idea of the endpoints library is that you first define an abstract description of your HTTP endpoints and then the library provides:

  • a server implementation decoding requests and building responses,
  • a client implementation building requests and decoding responses,
  • a machine readable documentation (OpenAPI document).

Project layout

The typical setup consists in a multi-project build, with a client project and a server project both depending on a shared project.

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

Dependencies

The shared project has to depend on so-called algebras, which provide the vocabulary to describe the communication endpoints, and the client and server projects have to depend on interpreters, which give a concrete meaning to the endpoint descriptions. See the algebras and interpreters page for an exhaustive list.

In this example you will use the following dependencies:

val shared =
  crossProject(JSPlatform, JVMPlatform).crossType(CrossType.Pure).settings(
    libraryDependencies ++= Seq(
      "org.julienrf" %%% "endpoints-algebra" % "0.15.0",
      // optional, see explanation below
      "org.julienrf" %%% "endpoints-json-schema-generic" % "0.15.0"
    )
  )

val sharedJS = shared.js
val sharedJVM = shared.jvm

val client =
  project.enablePlugins(ScalaJSPlugin).settings(
    libraryDependencies += "org.julienrf" %%% "endpoints-xhr-client" % "0.15.0"
  ).dependsOn(sharedJS)

val server =
  project.settings(
    libraryDependencies ++= Seq(
      "org.julienrf" %% "endpoints-akka-http-server" % "0.15.0",
      "org.scala-stm" %% "scala-stm" % "0.8"
    )
  ).dependsOn(sharedJVM)

The shared project uses the endpoints-json-schema-generic module in addition to the required algebra interface endpoints-algebra, to define the communication endpoints and to automatically derive the JSON schemas of the entities from their Scala type definitions.

The client project uses a Scala.js web client interpreter.

Finally, the server project uses a server interpreter backed by Akka HTTP. It also uses the scala-stm library for implementing the business logic.

Description of the HTTP endpoints

In the shared project, define a CounterEndpoints trait describing two endpoints, one for getting a counter value and one for incrementing it:

import endpoints.{algebra, generic}

/**
  * 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.
  */
trait CounterEndpoints
    extends algebra.Endpoints
    with algebra.JsonEntitiesFromSchemas
    with generic.JsonSchemas {

  /**
    * 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[Unit, Counter] =
    endpoint(get(path / "current-value"), ok(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[Increment, Unit] =
    endpoint(
      post(path / "increment", jsonRequest[Increment]),
      ok(emptyResponse)
    )

  // Generically derive the JSON schema of our `Counter`
  // and `Increment` case classes defined thereafter
  implicit lazy val counterSchema: JsonSchema[Counter] = genericJsonSchema
  implicit lazy val incrementSchema: JsonSchema[Increment] = genericJsonSchema

}

case class Counter(value: Int)
case class Increment(step: Int)

The currentValue and increment members define the endpoints for getting the counter current value or incrementing it, as their names suggest. The counterSchema and incrementSchema members define a JSON schema that will be used to serialize and deserialize the request and response entities.

Client implementation

A client implementation of the endpoints can be obtained by mixing so-called “interpreters” to the CounterEndpoints trait defined above. In this example, you want to get a JavaScript (Scala.js) client that uses XMLHttpRequest under the hood. Defines the following CounterClient object in the client project:

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.JsonEntitiesFromSchemas

And then, the CounterClient object 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(())

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))

As you can see, invoking an endpoint consists of calling a function on the CounterClient object. The endpoints library then builds an HTTP request (according to the endpoint description), sends it to the server, and eventually decodes the HTTP response (according to the endpoint description).

Server implementation

Similarly, a server implementation of the endpoints can be obtained by mixing the appropriate interpreters to the CounterEndpoints trait. In this example, you want to get a JVM server that uses Akka HTTP under the hood. Create the following CounterServer class in the server project:

import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.server.Route
import endpoints.akkahttp.server

import scala.concurrent.stm.Ref

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

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

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

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

  val routes: Route =
    currentValueRoute ~ incrementRoute

}

The routes value produced by the endpoints library is a Route value directly usable by Akka HTTP. The last section shows how to setup an Akka HTTP server that uses these routes.

The routes implementations provided by endpoints decode the incoming HTTP requests, call the corresponding logic (here, incrementing the counter or getting its current value), and build the HTTP responses.

Documentation generation

You can also generate documentation for the endpoints, again by mixing the appropriate interpreters. Create the following CounterDocumentation object in the server project:

import endpoints.openapi
import endpoints.openapi.model.{Info, OpenApi}

/**
  * Generates OpenAPI documentation for the endpoints described in the `CounterEndpoints` trait.
  */
object CounterDocumentation
    extends CounterEndpoints
    with openapi.Endpoints
    with openapi.JsonEntitiesFromSchemas {

  val api: OpenApi =
    openApi(
      Info(title = "API to manipulate a counter", version = "1.0.0")
    )(currentValue, increment)

}

This code defines a CounterDocumentation object with an api member containing an OpenAPI object documenting the currentValue and increment endpoints.

Running the application

Finally, to run your application you need to build a proper Akka HTTP server serving your routes. Define the following Main object:

import akka.actor.ActorSystem
import akka.http.scaladsl.Http
import akka.http.scaladsl.server.Directives._

object Main extends App {
  implicit val system: ActorSystem = ActorSystem("server-system")
  val routes = CounterServer.routes ~ DocumentationServer.routes
  Http().bindAndHandle(routes, "0.0.0.0", 8000)
}

// Additional route for serving the OpenAPI documentation
import endpoints.openapi.model.OpenApi
import endpoints.akkahttp.server

object DocumentationServer
    extends server.Endpoints
    with server.JsonEntitiesFromEncodersAndDecoders {

  val routes =
    endpoint(get(path / "documentation.json"), ok(jsonResponse[OpenApi]))
      .implementedBy(_ => CounterDocumentation.api)

}

You can then browse the http://localhost:8000/current-value URL to query the counter value, or the http://localhost:8000/documentation.json URL to get the generated OpenAPI documentation, which should look like the following:

{
  "openapi": "3.0.0",
  "info": {
    "title": "API to manipulate a counter",
    "version": "1.0.0"
  },
  "components": {
    "schemas": {
      "quickstart.Counter": {
        "type": "object",
        "properties": {
          "value": {
            "format": "int32",
            "type": "integer"
          }
        },
        "required": ["value"]
      },
      "quickstart.Increment": {
        "type": "object",
        "properties": {
          "step": {
            "format": "int32",
            "type": "integer"
          }
        },
        "required": ["step"]
      },
      "endpoints.Errors": {
        "type": "array",
        "items": {
          "type": "string"
        }
      }
    }
  },
  "paths": {
    "/increment": {
      "post": {
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/quickstart.Increment"
              }
            }
          }
        },
        "responses": {
          "400": {
            "description": "Client error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/endpoints.Errors"
                }
              }
            }
          },
          "500": {
            "description": "Server error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/endpoints.Errors"
                }
              }
            }
          },
          "200": {
            "description": ""
          }
        }
      }
    },
    "/current-value": {
      "get": {
        "responses": {
          "400": {
            "description": "Client error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/endpoints.Errors"
                }
              }
            }
          },
          "500": {
            "description": "Server error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/endpoints.Errors"
                }
              }
            }
          },
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/quickstart.Counter"
                }
              }
            }
          }
        }
      }
    }
  }
}

Next Step

Learn about the design principles of the endpoints library.