ApiDefinition.md
September 7, 2018 ยท View on GitHub
How to define an API
The central idea behind Typedapi is to make client and server implementation as boilerplate-free, typesafe and simple as possible.
- On the client-side you only define what you expect from an API provided by a server. In other words, you define a contract between the client and the server.
- The server-side then has to comply with that contract by implementing proper endpoint functions.
But how do you create this API definitions/contracts? This document will show you two ways provided by Typedapi:
- use the DSL (
import typedapi.dsl._) - or function-call-like definition (
import typedapi._)
Base case
Every API has to fullfil the base case, meaning it has to have a root path and a method description:
// dsl
:= :> Get[MediaTypes.`application/json`, A]
// or
:= :> Get[MT.`application/json`, A]
// or in case of JSON
:= :> Get[Json, A]
// function
api(Get[Json, A])
// or
api(method = Get[Json, A], path = Root)
This translates to GET / returning some Json A.
Methods
So far Typedapi supports the following methods:
// dsl
:= :> Get[Json, A]
:= :> Put[Json, A]
:= :> Post[Json, A]
:= :> Delete[Json, A]
// function
api(Get[Json, A])
api(Put[Json, A])
api(Post[Json, A])
api(Delete[Json, A])
Request Body
You may noticed that Put and Post don't have a field to describe a request body. To add that you have to explicitly define it with an element in your Api:
// PUT {body: User} /
// dsl
:= :> ReqBody[Json, B] :> Put[Json, A]
// function
apiWithBody(Put[Json, A], ReqBody[Json, B])
// POST {body: User} /
// dsl
:= :> ReqBody[Json, B] :> Post[Json, A]
// function
apiWithBody(Post[Json, A], ReqBody[Json, B])
By the way, you can only add Put and Post as the next element of ReqBody. Everything else will not compile. Thus, you end up with a valid API description and not something like := :> ReqBody[Json, B] :> Get[Json, A] or api(Get[Json, A], ReqBody[Json, B]).
One word to encodings
You can find a list of provided encodings here. If you need something else implement trait MediaType.
Path
// GET /hello/world
// dsl
:= :> "hello" :> "world" :> Get[Json, A]
// function
api(Get[Json, A], Root / "hello" / "world")
All path elements are translated to singleton types and therefore encoded in the type of the API.
Segment
Have a dynamic path element:
// GET /user/{name: String}
// dsl
:= :> "user" :> Segment[String]("name") :> Get[Json, A]
// function
api(Get[Json, A], Root / "user" / Segment[String]("name"))
Every segment gets a name which is again encoded as singleton type in the API type.
Query Parameter
// GET /query?{id: Int}
// dsl
:= :> "query" :> Query[Int]("id") :> Get[Json, A]
// function
api(Get[Json, A], Root / "query", Queries.add[Int]("id"))
Every query gets a name which is again encoded as singleton type in the API type.
Optional Query
// GET /query/opt?{id: Option[Int]}
// dsl
:= :> "query" :> "opt" :> Query[Option[Int]]("id") :> Get[Json, A]
// function
api(Get[Json, A], Root / "query" / "opt", Queries.add[Option[Int]]("id"))
Query with a List of elements
// GET /query/list?{id: List[Int]}
// dsl
:= :> "query" :> "list" :> Query[List[Int]]("id") :> Get[Json, A]
// function
api(Get[Json, A], Root / "query" / "list", Queries.add[List[Int]]("id"))
Header
// GET /header {headers: id: Int}
// dsl
:= :> "header" :> Header[Int]("id") :> Get[Json, A]
// function
api(Get[Json, A], Root / "header", headers = Headers.add[Int]("id"))
This header is an expected input parameter.
Every header gets a name which is again encoded as singleton type in the API type.
Optional Header
// GET /header/opt {headers: id: Option[Int]}
// dsl
:= :> "header" :> "opt" :> Header[Option[Int]]("id") :> Get[Json, A]
// function
api(Get[Json, A], Root / "header" / "opt", headers = Headers.add[Option[Int]]("id"))
Fixed Headers aka static headers
If you have a set of headers which are statically known and have to be provided by all sides you can add them as follows:
// GET /header/fixed {headers: consumer=me}
// dsl
:= :> "header" :> "fixed" :> Header("consumer", "me") :> Get[Json, A]
// function
api(Get[Json, A], Root / "header" / "fixed", headers = Headers.add("consumer", "me"))
Client-Side: Headers
You have to send headers from the client-side but not server side? Here you go:
// GET /header/client {header: consumer: String}
// dsl
:= :> "header" :> "client" :> Client.Header[String]("consumer") :> Get[Json, A]
// function
api(Get[Json, A], Root / "header" / "client", headers = Headers.client[String]("consumer"))
Client-Side: fixed/static Headers
You have to send static headers from the client-side but not server side? Here you go:
// GET /header/client/fixed {header: consumer=me}
// dsl
:= :> "header" :> "client" :> "fixed" :> Client.Header("consumer", "me") :> Get[Json, A]
// function
api(Get[Json, A], Root / "header" / "client" / "fixed", headers = Headers.client("consumer", "me"))
Client-Side: Header collections
You can send header collections (Map[String, V]) as a single argument:
// GET /header/client/coll {headers: a:b:...}
//dsl
:= :> "header" :> "client" :> "coll" :> Client.Coll[Int] :> Get[Json, A]
//function
api(Get[Json, A], Root / "header" / "client" / "coll", headers = Headers.clientColl[Int])
Server-Side: send Headers
You have to send headers from the server-side, e.g. for CORS? Here you go:
// GET /header/server/send => {header: consumer=me}
// dsl
:= :> "header" :> "server" :> "send" :> Server.Send("consumer", "me") :> Get[Json, A]
// function
api(Get[Json, A], Root / "header" / "server" / "send", headers = Headers.serverSend("consumer", "me"))
Server-Side: extract matching Headers keys
You want to extract all headers which contain a certain String? Here you go:
// GET /header/server/match {header: test1=me, test2=you}
// dsl
:= :> "header" :> "server" :> "match" :> Server.Match[String]("test") :> Get[Json, A]
// function
api(Get[Json, A], Root / "header" / "server" / "match", headers = Headers.serverMatch[String]("test"))
This will give you a Set[V] with V = String in this example.
Multiple definitions in a single API
You can put multiple definitions into a single API element:
val Api =
(:= :> "hello" :> Get[Json, A]) :|:
(:= :> "world" :> Query[Int]('foo) :> Delete[B])