How does one scale the development of a service landscape in a corporate enterprise environment utilizing Typesafe's Play and Akka software stack? How does one achieve API uniformity and coherence accross dozens of development teams, getting them and their subsequently developed subsystems to play together nicely? At Zalando we believe firmly in an API first approach, founded an API guild that ratifies and supports the development of APIs, and define them in a formal manner employing the Swagger API representation language.
22. API First
Document and
peer review API
before writing a
single line of code
Ideally, generate either
your server interfaces
or your test data (or
both) from the spec
26. What is REST’s central distinguishing feature?
… its emphasis on a uniform interface between
components
Roy Thomas Fielding
remaining the same in all cases and at all
times
British Dictionary
uniform
29. REST
• Client-Server
• Stateless
• Cacheable
• Layered System
• Uniform Interface
• Identification of resources
• Manipulation of resources through these representations
• Self-descriptive messages
• Hypermedia as the engine of application state
implementation details
Server
Client/Server
Protocol
39. DRY
Most people take DRY to mean you shouldn't duplicate code.
That's not its intention. The idea behind DRY is far grander
than that. DRY says that every piece of system knowledge
should have one authoritative, unambiguous representation.
Dave Thomas
41. • Easy to use
• Human readable
• Widest adoption
• Open Source
• Scala and Java
• Dynamic recompilation / Hot reload
• Asynchronous IO
• Easy to use
42. • URI path definitions (supports parameterisation and templating)
• URI parameter definitions
• Response definitions
• Scheme definitions
• MIME type definitions
• Primitive datatypes
• Complex datatypes
• Structural constraints
• Value constraints
• Security constraints
• Tags
• Vendor extensions
50. Metadata
swagger: "2.0"
info:
version: 1.0.0
title: Swagger Petstore
description: A sample API that uses a petstore as an example to
demonstrate features in the swagger-2.0 specification
termsOfService: http://swagger.io/terms/
contact:
name: Swagger API Team
email: foo@example.com
url: http://madskristensen.net
license:
name: MIT
url: http://github.com/gruntjs/grunt/blob/master/LICENSE-MIT
host: petstore.swagger.io
basePath: /api
schemes:
- http
consumes:
- application/json
produces:
- application/json
URL Prefix
52. Test data
definitions:
Pet:
allOf:
- $ref: '#/definitions/NewPet'
- required:
- id
properties:
id:
type: integer
format: int64
NewPet:
required:
- name
properties:
name:
type: string
tag:
type: string
Error:
required:
- code
- message
properties:
code:
type: integer
format: int32
message:
type: string
object generatorDefinitions {
def createPet = _generate(PetGenerator)
def createNewPet = _generate(NewPetGenerator)
def createError = _generate(ErrorGenerator)
// test data generator for /definitions/Pet
val PetGenerator =
for {
id <- Gen.option(arbitrary[Long])
name <- arbitrary[String]
tag <- Gen.option(arbitrary[String])
} yield Pet(id, name, tag)
// test data generator for /definitions/NewPet
val NewPetGenerator =
for {
name <- arbitrary[String]
tag <- Gen.option(arbitrary[String])
} yield NewPet(name, tag)
// test data generator for /definitions/Error
val ErrorGenerator =
for {
code <- arbitrary[Int]
message <- arbitrary[String]
} yield Error(code, message)
def _generate[T](gen: Gen[T]) = (count: Int) =>
for (i <- 1 to count) yield gen.sample
}
53. Validations'#/definitions/NewPet'
red:
rties:
ype: integer
ormat: int64
s:
string
string
ge
s:
integer
t: int32
:
string
class PetValidation(instance: Pet) {
import de.zalando.play.controllers.PlayValidations._
val allValidations = Seq.empty[scala.Either[scala.Seq[ParsingError], String]]
val result = {
val errors = allValidations.filter(_.isLeft).flatMap(_.left.get)
if (errors.nonEmpty) Left(errors) else Right(instance)
}
}
class NewPetValidation(instance: NewPet) {
import de.zalando.play.controllers.PlayValidations._
val allValidations = Seq.empty[scala.Either[scala.Seq[ParsingError], String]]
val result = {
val errors = allValidations.filter(_.isLeft).flatMap(_.left.get)
if (errors.nonEmpty) Left(errors) else Right(instance)
}
}
class ErrorValidation(instance: Error) {
import de.zalando.play.controllers.PlayValidations._
val allValidations = Seq.empty[scala.Either[scala.Seq[ParsingError], String]]
val result = {
val errors = allValidations.filter(_.isLeft).flatMap(_.left.get)
if (errors.nonEmpty) Left(errors) else Right(instance)
}
}
54. Validations
/pets/{id}:
get:
description: Returns a user based
on a single ID, if the user does not
have access to the pet
operationId: find pet by id
parameters:
- name: id
in: path
description: ID of pet to fetch
required: true
type: integer
format: int64
responses:
200:
description: pet response
schema:
$ref: '#/definitions/Pet'
default:
description: unexpected error
schema:
$ref: '#/definitions/Error'
class ValidationForPetexpandedYamlfindPetById(in: (Long)) {
val (id) = in
val idConstraints = new ValidationBase[Long] {
override def constraints: Seq[Constraint[Long]] = Seq()
}
val normalValidations =
Seq(idConstraints.applyConstraints(id))
val containerValidations =
Seq.empty[scala.Either[scala.Seq[ParsingError], String]]
val rightResult = Right((id))
val allValidations = normalValidations ++
containerValidations
val result = {
val errors =
allValidations.filter(_.isLeft).flatMap(_.left.get)
if (errors.nonEmpty) Left(errors) else rightResult
}
}
55. Tests
"discard invalid data" in new WithApplication {
val genInputs =
for {
id <- arbitrary[Long]
} yield (id)
val inputs = genInputs suchThat { i => new ValidationForPetexpandedYamlfindPetById(i).result !=
Right(i) }
val props = forAll(inputs) { i => testInvalidInput(i) }
checkResult(props)
}
56. Tests
def testInvalidInput(in: (Long)) = {
val (id) = in
val url = s"""/api/pets/${id}"""
val path = route(FakeRequest(GET, url)).get
val validation = new ValidationForPetexpandedYamlfindPetById(id).result
lazy val validations = validation.left.get flatMap {
_.messages map { m => contentAsString(path).contains(m) ?= true }
}
("given an URL: [" + url + "]") |: all(
status(path) ?= BAD_REQUEST,
contentType(path) ?= Some("application/json"),
validation.isLeft ?= true,
all(validations:_*)
)
}
57. Controllers
private val findPetByIdResponseMimeType = "application/json"
private val findPetByIdActionSuccessStatus = Status(200)
private type findPetByIdActionRequestType = (Long)
private type findPetByIdActionResultType = Pet
private type findPetByIdActionType = findPetByIdActionRequestType => Either[Throwable, findPetByIdActionResultType]
private def errorToStatusfindPetById: PartialFunction[Throwable, Status] = PartialFunction.empty[Throwable, Status]
def findPetByIdAction = (f: findPetByIdActionType) => (id: Long) => Action {
val result = new ValidationForPetexpandedYamlfindPetById(id).result.right.map {
processValidfindPetByIdRequest(f)
}
implicit val marshaller = parsingErrors2Writable(findPetByIdResponseMimeType)
val response = result.left.map { BadRequest(_) }
response.fold(a => a, c => c)
}
private def processValidfindPetByIdRequest(f: findPetByIdActionType)(request: findPetByIdActionRequestType) = {
val callerResult = f(request)
val status = callerResult match {
case Left(error) => (errorToStatusfindPetById orElse defaultErrorMapping)(error)
case Right(result) => findPetByIdActionSuccessStatus
}
implicit val findPetByIdWritableJson = anyToWritable[findPetByIdActionResultType](findPetByIdResponseMimeType)
status(callerResult)
}
58. Skeletons
class PetexpandedYaml extends PetexpandedYamlBase {
// handler for GET /pets
def findPets = findPetsAction { in : (Option[Seq[String]], Option[Int]) =>
val (tags, limit) = in
???
}
// handler for POST /pets
def addPet = addPetAction { in : (NewPet) =>
val (pet) = in
???
}
// handler for GET /pets/{id}
def findPetById = findPetByIdAction { in : (Long) =>
val (id) = in
???
}
// handler for DELETE /pets/{id}
def deletePet = deletePetAction { in : (Long) =>
val (id) = in
???
}
}