Slide 1

Slide 1 text

A SERVER IS JUST A FUNCTION A N I N T R O D U C T I O N TO H T T P 4 S J A K U B KO Z Ł O W S K I

Slide 2

Slide 2 text

WHAT MAKES A SERVER?

Slide 3

Slide 3 text

WHAT MAKES A FUNCTION?

Slide 4

Slide 4 text

type Server = Request => Response? ?

Slide 5

Slide 5 text

No content

Slide 6

Slide 6 text

WHAT REALLY MAKES A SERVER? Streaming Shared resources State Errors Routing Auth Serialization

Slide 7

Slide 7 text

HTTP4S Purely functional, streaming HTTP server/client https://http4s.org

Slide 8

Slide 8 text

HTTP4S Purely functional, streaming HTTP server/client Multiple backends https://http4s.org

Slide 9

Slide 9 text

HTTP4S Purely functional, streaming HTTP server/client Multiple backends Built on cats-effect and fs2 https://http4s.org

Slide 10

Slide 10 text

HTTP4S Purely functional, streaming HTTP server/client Multiple backends Built on cats-effect and fs2 Supports websockets (server) https://http4s.org

Slide 11

Slide 11 text

Function1[_, _]

Slide 12

Slide 12 text

Function1[_, _] + IO[_]

Slide 13

Slide 13 text

Function1[_, _] + IO[_]

Slide 14

Slide 14 text

HttpApp = Request => IO[Response] HttpRoutes = Request => IO[Option[Response]] HTTP4S SERVER: INTUITION

Slide 15

Slide 15 text

Kleisli

Slide 16

Slide 16 text

No content

Slide 17

Slide 17 text

Slide 18

Slide 18 text

KLEISLI case class Kleisli[F[_], A, B]( run: A => F[B] )

Slide 19

Slide 19 text

KLEISLI case class Kleisli[F[_], A, B]( run: ) Function with effectful result A => F[B]

Slide 20

Slide 20 text

KLEISLI val len: Kleisli[IO, Token, Int] = Kleisli { token => IO.pure(token.length) } val cloned: Kleisli[IO, Token, String] = Kleisli { token => IO.pure(token + token) }

Slide 21

Slide 21 text

KLEISLI val f3 = for { a <- len b <- cloned } yield (a + b) val f4 = (len, cloned).tupled val len: Kleisli[IO, Token, Int] = Kleisli { token => IO.pure(token.length) } val cloned: Kleisli[IO, Token, String] = Kleisli { token => IO.pure(token + token) }

Slide 22

Slide 22 text

OptionT

Slide 23

Slide 23 text

OPTIONT case class OptionT[F[_], A]( value: F[Option[A]] )

Slide 24

Slide 24 text

OPTIONT Option nested in an effect case class OptionT[F[_], A]( value: ) F[Option[A]]

Slide 25

Slide 25 text

OPTIONT val num: OptionT[IO, Int] = OptionT(IO.pure(42.some)) def text(num: Int): OptionT[IO, String] = num match { case n if n % 2 === 0 => OptionT(IO.pure("foo".some)) case _ => OptionT.none }

Slide 26

Slide 26 text

OPTIONT val num: OptionT[IO, Int] = OptionT(IO.pure(42.some)) def text(num: Int): OptionT[IO, String] = num match { case n if n % 2 === 0 => OptionT(IO.pure("foo".some)) case _ => OptionT.none } val result = for { n <- num s <- text(n) s2 <- text(n + 1) s3 <- text(n + 2) } yield s + s2 + s3 val unwrapped: IO[Option[String]] = result.value

Slide 27

Slide 27 text

HttpApp = Kleisli[IO, Request, Response] HttpRoutes = Kleisli[OptionT[IO, ?], Request, Response] HTTP4S SERVER: THE REAL THING (ALMOST)

Slide 28

Slide 28 text

HTTP4S SERVER: THE REAL REAL THING HttpApp[F[_]] = Kleisli[F, Request[F], Response[F]] HttpRoutes[F[_]] = Kleisli[OptionT[F, ?], Request[F], Response[F]] Effect type of entity body stream

Slide 29

Slide 29 text

HTTP4S SERVER: THE REAL REAL THING Http[F[_], G[_]] = Kleisli[F, Request[G], Response[G]] HttpApp[F[_]] = Http[F, F] HttpRoutes[F[_]] = Http[OptionT[F, ?], F]

Slide 30

Slide 30 text

ALL THIS ABSTRACTION... WHY? (STAY TUNED) Spoiler: DRY

Slide 31

Slide 31 text

ROUTING DSL

Slide 32

Slide 32 text

object Main extends IOApp { val routes = HttpRoutes.of[IO] { case GET -> Root / "hello" => Ok("Hello, world!") } def run(args: List[String]): IO[ExitCode] = BlazeServerBuilder[IO] .withHttpApp(routes.orNotFound) .bindHttp(port = 8080) .resource .use(_ => IO.never) }

Slide 33

Slide 33 text

object Main extends IOApp { val routes = HttpRoutes.of[IO] { case GET -> Root / "hello" => Ok("Hello, world!") } def run(args: List[String]): IO[ExitCode] = BlazeServerBuilder[IO] .withHttpApp( ) .bindHttp(port = 8080) .resource .use(_ => IO.never) } routes.orNotFound HttpRoutes => HttpApp

Slide 34

Slide 34 text

object Main extends IOApp { val routes = HttpRoutes.of[IO] { case GET -> Root / "hello" => Ok("Hello, world!") } def run(args: List[String]): IO[ExitCode] = BlazeServerBuilder[IO] .withHttpApp(routes.orNotFound) .bindHttp(port = 8080) .resource .use(_ => IO.never) }

Slide 35

Slide 35 text

HttpRoutes.of[IO] { case GET -> Root / "hello" => Ok("Hello, world!") }

Slide 36

Slide 36 text

HttpRoutes.of[IO] { case -> Root / "hello" => Ok("Hello, world!") } GET

Slide 37

Slide 37 text

HttpRoutes.of[IO] { case GET -> => Ok("Hello, world!") } Root / "hello"

Slide 38

Slide 38 text

Ok("Hello, world!") HttpRoutes.of[IO] { case GET -> Root / "hello" => }

Slide 39

Slide 39 text

HttpRoutes.of[IO] { case GET -> Root / "hello" => Ok("Hello, world!") }

Slide 40

Slide 40 text

HttpRoutes.of[IO] { case request @ POST -> Root / "echo" => Ok(request.body) }

Slide 41

Slide 41 text

HttpRoutes.of[IO] { case request @ POST -> Root / "echo" => Ok( ) } request.body

Slide 42

Slide 42 text

HttpRoutes.of[IO] { case request @ POST -> Root / "echo" => Ok( ) } request.body Literally Stream[IO, Byte]

Slide 43

Slide 43 text

CLIENT

Slide 44

Slide 44 text

val request = Request[IO](uri = uri"https://http4s.org") val remoteCall: IO[String] = client.expect[String](request)

Slide 45

Slide 45 text

val request = Request[IO](uri = uri"https://http4s.org") val remoteCall: IO[String] = client.expect[String](request) remoteCall BlazeClientBuilder[IO](executionContext).resource.use { client => }

Slide 46

Slide 46 text

case GET -> Root / "remote" / "stream" => val response = client.stream(request).flatMap(_.body) Ok(response)

Slide 47

Slide 47 text

trait Client[F[_]] { def run(req: Request[F]): Resource[F, Response[F]] //+ a bunch of other convenient methods }

Slide 48

Slide 48 text

trait Client[F[_]] { def run(req: Request[F]): Resource[F, Response[F]] } def apply[F[_]](f: Request[F] => Resource[F, Response[F]] ): Client[F]

Slide 49

Slide 49 text

trait Client[F[_]] { def run(req: Request[F]): Resource[F, Response[F]] } def apply[F[_]](f: Request[F] => Resource[F, Response[F]] ): Client[F]

Slide 50

Slide 50 text

trait Client[F[_]] { def run(req: Request[F]): Resource[F, Response[F]] } def fromHttpApp[F[_]](app: HttpApp[F]): Client[F] def apply[F[_]](f: Request[F] => Resource[F, Response[F]] ): Client[F]

Slide 51

Slide 51 text

T E S T I N G R E S O U R C E S A F E T Y E X T E N S I B I L I T Y

Slide 52

Slide 52 text

T E S T I N G R E S O U R C E S A F E T Y E X T E N S I B I L I T Y

Slide 53

Slide 53 text

TESTING

Slide 54

Slide 54 text

TESTING Server routes

Slide 55

Slide 55 text

TESTING Server routes Server HTTP interface

Slide 56

Slide 56 text

TESTING Server routes Server HTTP interface Client calls

Slide 57

Slide 57 text

TESTING ROUTES def routes(client: Client[IO]): HttpRoutes[IO] = { HttpRoutes.of[IO] { //... case GET -> Root / "remote" => val remoteCall: IO[String] = client.expect[String](request) for { result <- remoteCall response <- Ok(result) } yield response //... } }

Slide 58

Slide 58 text

val remoteClient = Client.fromHttpApp(HttpApp.notFound[IO]) TESTING ROUTES

Slide 59

Slide 59 text

val remoteClient = Client.fromHttpApp(HttpApp.notFound[IO]) val routes = Main.routes(remoteClient) TESTING ROUTES

Slide 60

Slide 60 text

val body = Json.obj("foo" -> Json.fromString("bar")) val request = Request[IO](method = Method.POST, uri = uri"/echo").withEntity(body) val remoteClient = Client.fromHttpApp(HttpApp.notFound[IO]) val routes = Main.routes(remoteClient) httsps://http4s.org TESTING ROUTES

Slide 61

Slide 61 text

routes.run(request).value.flatMap(_.value.as[Json]).map(_ shouldBe body) val remoteClient = Client.fromHttpApp(HttpApp.notFound[IO]) val routes = Main.routes(remoteClient) val body = Json.obj("foo" -> Json.fromString("bar")) val request = Request[IO](method = Method.POST, uri = uri"/echo").withEntity(body) TESTING ROUTES

Slide 62

Slide 62 text

routes.run(request).value.flatMap(_.value.as[Json]).map(_ shouldBe body) val remoteClient = Client.fromHttpApp(HttpApp.notFound[IO]) val routes = Main.routes(remoteClient) val body = Json.obj("foo" -> Json.fromString("bar")) val request = Request[IO](method = Method.POST, uri = uri"/echo").withEntity(body) TESTING ROUTES Client.fromHttpApp(routes.orNotFound).expect[Json](request).map(_ shouldBe body) or

Slide 63

Slide 63 text

TESTING HTTP val blazeClient = BlazeClientBuilder[IO](ExecutionContext.global).resource

Slide 64

Slide 64 text

TESTING HTTP val blazeClient = BlazeClientBuilder[IO](ExecutionContext.global).resource Main.server.use { server => }

Slide 65

Slide 65 text

TESTING HTTP val blazeClient = BlazeClientBuilder[IO](ExecutionContext.global).resource Main.server.use { server => blazeClient.use { client => } }

Slide 66

Slide 66 text

TESTING HTTP val blazeClient = BlazeClientBuilder[IO](ExecutionContext.global).resource Main.server.use { server => blazeClient.use { client => } } client.expect[String](server.baseUri / "hello") .map(_ shouldBe "Hello world!")

Slide 67

Slide 67 text

TESTING CLIENTS class TodoClient(client: Client[IO]) { def getTodo(id: Int): IO[Todo] = client.expect[Todo](Request[IO](uri = uri"/todos" / id.toString)) }

Slide 68

Slide 68 text

TESTING CLIENTS class TodoClient(client: Client[IO]) { def getTodo(id: Int): IO[Todo] = client.expect[Todo](Request[IO](uri = uri"/todos" / id.toString)) } //in test val mockServer = HttpRoutes .of[IO] { case GET -> Root / "todos" / IntVar(id) => Ok(Todo(id, false)) } .orNotFound

Slide 69

Slide 69 text

TESTING CLIENTS val mockServer = HttpRoutes .of[IO] { case GET -> Root / "todos" / IntVar(id) => Ok(Todo(id, false)) } .orNotFound

Slide 70

Slide 70 text

TESTING CLIENTS val clientRaw = Client.fromHttpApp(mockServer) val todoClient = new TodoClient(clientRaw) val mockServer = HttpRoutes .of[IO] { case GET -> Root / "todos" / IntVar(id) => Ok(Todo(id, false)) } .orNotFound

Slide 71

Slide 71 text

TESTING CLIENTS val mockServer = HttpRoutes .of[IO] { case GET -> Root / "todos" / IntVar(id) => Ok(Todo(id, false)) } .orNotFound val clientRaw = Client.fromHttpApp(mockServer) val todoClient = new TodoClient(clientRaw) todoClient.getTodo(1).map(_ shouldBe Todo(1, false))

Slide 72

Slide 72 text

T E S T I N G R E S O U R C E S A F E T Y E X T E N S I B I L I T Y

Slide 73

Slide 73 text

T E S T I N G R E S O U R C E S A F E T Y E X T E N S I B I L I T Y

Slide 74

Slide 74 text

CATS.EFFECT.RESOURCE

Slide 75

Slide 75 text

RESOURCE abstract class Resource[F[_], A] { def use[B](f: A => F[B]): F[B] } object Resource { def make[F[_], A](acquire: F[A]) (release: A => F[Unit]): Resource[F, A] }

Slide 76

Slide 76 text

RESOURCE Resource.make(IO(createPool()))(pool => IO(pool.close()))

Slide 77

Slide 77 text

RESOURCE db <- Resource.make(IO(createPool()))(pool => IO(pool.close()))

Slide 78

Slide 78 text

RESOURCE val server = for { db <- Resource.make(IO(createPool()))(pool => IO(pool.close())) server <- BlazeServerBuilder[IO].withHttpApp(routes(db)) } yield server server.use(_ => IO.sleep(10.seconds))

Slide 79

Slide 79 text

RESOURCE val server = for { db <- Resource.make(IO(createPool()))(pool => IO(pool.close())) server <- BlazeServerBuilder[IO].withHttpApp(routes(db)) } yield server server.use(_ => IO.sleep(10.seconds)) 1. Create pool

Slide 80

Slide 80 text

RESOURCE val server = for { db <- Resource.make(IO(createPool()))(pool => IO(pool.close())) server <- BlazeServerBuilder[IO].withHttpApp(routes(db)) } yield server server.use(_ => IO.sleep(10.seconds)) 1. Create pool 2. Start server

Slide 81

Slide 81 text

RESOURCE val server = for { db <- Resource.make(IO(createPool()))(pool => IO(pool.close())) server <- BlazeServerBuilder[IO].withHttpApp(routes(db)) } yield server server.use(_ => IO.sleep(10.seconds)) 1. Create pool 2. Start server 3. Sleep 10 seconds

Slide 82

Slide 82 text

RESOURCE val server = for { db <- Resource.make(IO(createPool()))(pool => IO(pool.close())) server <- BlazeServerBuilder[IO].withHttpApp(routes(db)) } yield server server.use(_ => IO.sleep(10.seconds)) 1. Create pool 2. Start server 3. Sleep 10 seconds 4. Stop server

Slide 83

Slide 83 text

RESOURCE val server = for { db <- Resource.make(IO(createPool()))(pool => IO(pool.close())) server <- BlazeServerBuilder[IO].withHttpApp(routes(db)) } yield server server.use(_ => IO.sleep(10.seconds)) 1. Create pool 2. Start server 3. Sleep 10 seconds 4. Stop server 5. Close pool

Slide 84

Slide 84 text

FS2.STREAM[F[_], A] - 0 to ∞ values - Can have effects in F - Supports resource acquisition/cleanup

Slide 85

Slide 85 text

FS2.STREAM[F[_], A] - 0 to ∞ values - Can have effects in F - Supports resource acquisition/cleanup Stream(1,2,3)

Slide 86

Slide 86 text

FS2.STREAM[F[_], A] - 0 to ∞ values - Can have effects in F - Supports resource acquisition/cleanup Stream(1,2,3) Stream.eval(IO(util.Random.nextInt))

Slide 87

Slide 87 text

FS2.STREAM[F[_], A] - 0 to ∞ values - Can have effects in F - Supports resource acquisition/cleanup Stream(1,2,3) Stream.eval(IO(util.Random.nextInt)) Stream.awakeDelay[IO](1.second)

Slide 88

Slide 88 text

FS2.STREAM[F[_], A] - 0 to ∞ values - Can have effects in F - Supports resource acquisition/cleanup Stream(1,2,3) Stream.eval(IO(util.Random.nextInt)) Stream.awakeDelay[IO](1.second).compile.toList: IO[List[FiniteDuration]]

Slide 89

Slide 89 text

FS2.STREAM[F[_], A] Stream(1,2,3) Stream.eval(IO(util.Random.nextInt)) Stream.awakeDelay[IO](1.second).compile.toList: IO[List[FiniteDuration]]

Slide 90

Slide 90 text

FS2.STREAM[F[_], A] Stream(1,2,3) Stream.eval(IO(util.Random.nextInt)) Stream.awakeDelay[IO](1.second).compile.toList: IO[List[FiniteDuration]] + Stream.bracket + Stream.resource

Slide 91

Slide 91 text

RESOURCE SAFETY IN HTTP4S

Slide 92

Slide 92 text

RESOURCE SAFETY IN HTTP4S

Slide 93

Slide 93 text

RESOURCE SAFETY IN HTTP4S BlazeServerBuilder[IO].resource: Resource[IO, Server[IO]]

Slide 94

Slide 94 text

RESOURCE SAFETY IN HTTP4S BlazeServerBuilder[IO].resource: Resource[IO, Server[IO]] BlazeClientBuilder[IO](ec).resource: Resource[IO, Client[IO]]

Slide 95

Slide 95 text

RESOURCE SAFETY IN HTTP4S BlazeServerBuilder[IO].resource: Resource[IO, Server[IO]] BlazeClientBuilder[IO](ec).resource: Resource[IO, Client[IO]] (request: Request[IO]).body: fs2.Stream[IO, Byte]

Slide 96

Slide 96 text

RESOURCE SAFETY IN HTTP4S BlazeServerBuilder[IO].resource: Resource[IO, Server[IO]] BlazeClientBuilder[IO](ec).resource: Resource[IO, Client[IO]] (request: Request[IO]).body: fs2.Stream[IO, Byte] (response: Response[IO]).body: fs2.Stream[IO, Byte]

Slide 97

Slide 97 text

RESOURCE SAFETY IN HTTP4S BlazeServerBuilder[IO].resource: Resource[IO, Server[IO]] BlazeClientBuilder[IO](ec).resource: Resource[IO, Client[IO]] (request: Request[IO]).body: fs2.Stream[IO, Byte] (response: Response[IO]).body: fs2.Stream[IO, Byte] ...and then there's Client

Slide 98

Slide 98 text

trait Client[F[_]] { def run(req: Request[F]) : Resource[F, Response[F]] def stream(req: Request[F]): Stream[F, Response[F]] }

Slide 99

Slide 99 text

trait Client[F[_]] { def run(req: Request[F]) : Resource[F, Response[F]] def stream(req: Request[F]): Stream[F, Response[F]] def fetch[A](req: Request[F])(f: Response[F] => F[A]) : F[A] def expect[A](req: Request[F])(implicit d: EntityDecoder[F, A]): F[A] } (these have like 10 overloads*) *some are about to be removed

Slide 100

Slide 100 text

trait Client[F[_]] { def run(req: Request[F]) : Resource[F, Response[F]] def stream(req: Request[F]): Stream[F, Response[F]] def fetch[A](req: Request[F])(f: Response[F] => F[A]) : F[A] def expect[A](req: Request[F])(implicit d: EntityDecoder[F, A]): F[A] def status(req: Request[F]). : F[Status] def successful(req: Request[F]): F[Boolean] }

Slide 101

Slide 101 text

trait Client[F[_]] { def run(req: Request[F]) : Resource[F, Response[F]] def stream(req: Request[F]): Stream[F, Response[F]] def fetch[A](req: Request[F])(f: Response[F] => F[A]) : F[A] def expect[A](req: Request[F])(implicit d: EntityDecoder[F, A]): F[A] def status(req: Request[F]). : F[Status] def successful(req: Request[F]): F[Boolean] def toKleisli[A](f: Response[F] => F[A]): Kleisli[F, Request[F], A] //... }

Slide 102

Slide 102 text

trait Client[F[_]] { def run(req: Request[F]) : Resource[F, Response[F]] def stream(req: Request[F]): Stream[F, Response[F]] def fetch[A](req: Request[F])(f: Response[F] => F[A]) : F[A] def expect[A](req: Request[F])(implicit d: EntityDecoder[F, A]): F[A] def status(req: Request[F]). : F[Status] def successful(req: Request[F]): F[Boolean] def toKleisli[A](f: Response[F] => F[A]): Kleisli[F, Request[F], A] //... //here be dragons - don't use unless you know what you're doing def toHttpApp: HttpApp[F] }

Slide 103

Slide 103 text

T E S T I N G R E S O U R C E S A F E T Y E X T E N S I B I L I T Y

Slide 104

Slide 104 text

T E S T I N G R E S O U R C E S A F E T Y E X T E N S I B I L I T Y

Slide 105

Slide 105 text

EXTENSIBILITY

Slide 106

Slide 106 text

EXTENSIBILITY If a server/client is still a function...

Slide 107

Slide 107 text

EXTENSIBILITY If a server/client is still a function... what can we do with functions?

Slide 108

Slide 108 text

case class Data(x: Int) val f: Data => Int = _.x

Slide 109

Slide 109 text

case class Data(x: Int) val f: Data => Int = _.x def wrapF(f1: Data => Int): Data => Int = data => f1(data.copy(x = data.x + 1)) * 2

Slide 110

Slide 110 text

case class Data(x: Int) val f: Data => Int = _.x def wrapF(f1: Data => Int): Data => Int = data => f1(data.copy(x = data.x + 1)) * 2 val f2: Data => Int = wrapF(f)

Slide 111

Slide 111 text

case class Data(x: Int) val f: Data => Int = _.x val f2: Data => Int = wrapF(f) def wrapF(f1: Data => Int): Data => Int = data => f1( ) * 2 data.copy(x = data.x + 1)

Slide 112

Slide 112 text

No content

Slide 113

Slide 113 text

No content

Slide 114

Slide 114 text

No content

Slide 115

Slide 115 text

HttpApp[F[_]] = Kleisli[F, Request[F], Response[F]]

Slide 116

Slide 116 text

SERVER MIDDLEWARE http: HttpApp[F]

Slide 117

Slide 117 text

SERVER MIDDLEWARE def apply[F[_]]( http: HttpApp[F]): HttpApp[F]

Slide 118

Slide 118 text

SERVER MIDDLEWARE: RESPONSE TIMING object ResponseTiming { def apply[F[_]]( http: HttpApp[F], timeUnit: TimeUnit = MILLISECONDS, headerName: CaseInsensitiveString = CaseInsensitiveString("X-Response-Time"))( implicit F: Sync[F], clock: Clock[F]): HttpApp[F] = Kleisli { req => for { before <- clock.monotonic(timeUnit) resp <- http(req) after <- clock.monotonic(timeUnit) header = Header(headerName.value, s"${after - before}") } yield resp.putHeaders(header) } }

Slide 119

Slide 119 text

SERVER MIDDLEWARE: RESPONSE TIMING object ResponseTiming { def apply[F[_]]( http: HttpApp[F], timeUnit: TimeUnit = MILLISECONDS, headerName: CaseInsensitiveString = CaseInsensitiveString("X-Response-Time"))( implicit F: Sync[F], clock: Clock[F]): HttpApp[F] = Kleisli { req => for { before <- clock.monotonic(timeUnit) after <- clock.monotonic(timeUnit) header = Header(headerName.value, s"${after - before}") } yield } } Kleisli { req => resp <- http(req) resp.putHeaders(header) }

Slide 120

Slide 120 text

SERVER MIDDLEWARE: HEADER ECHO object HeaderEcho { def apply[F[_]: Functor, G[_]: Functor] (echoHeadersWhen: CaseInsensitiveString => Boolean) (http: Http[F, G]): Http[F, G] = Kleisli { req: Request[G] => val headersToEcho = req.headers.filter(h => echoHeadersWhen(h.name)) http(req).map(_.putHeaders(headersToEcho.toList: _*)) } }

Slide 121

Slide 121 text

SERVER MIDDLEWARE: HEADER ECHO object HeaderEcho { def apply[F[_]: Functor, G[_]: Functor] (echoHeadersWhen: CaseInsensitiveString => Boolean) = Kleisli { req: Request[G] => val headersToEcho = req.headers.filter(h => echoHeadersWhen(h.name)) http(req).map(_.putHeaders(headersToEcho.toList: _*)) } } (http: Http[F, G]): Http[F, G]

Slide 122

Slide 122 text

CLIENT MIDDLEWARE client: Client[F]

Slide 123

Slide 123 text

CLIENT MIDDLEWARE def apply[F[_]]( client: Client[F] ): Client[F]

Slide 124

Slide 124 text

CLIENT MIDDLEWARE: BASE URL object BaseUrl { def apply[F[_]](base: Uri)( client: Client[F] )(implicit F: Bracket[F, Throwable]): Client[F] = { Client.apply[F] { req => client.run(req.withUri(base.resolve(req.uri))) } } }

Slide 125

Slide 125 text

SERVER CLIENT

Slide 126

Slide 126 text

LEARN MORE http4s.org gitter.im/http4s/http4s gitter.im/typelevel/cats-effect

Slide 127

Slide 127 text

ATTRIBUTION natural power by Icon Island from the Noun Project testing by tom from the Noun Project modules by mikicon from the Noun Project

Slide 128

Slide 128 text

THANK YOU Slides: bit.ly/2WaFCOM Code: git.io/fjs4v Twitter: @kubukoz My blog: blog.kubukoz.com

Slide 129

Slide 129 text

BONUS SLIDE: WEBSOCKETS case GET -> Root / "ws" / "echo" => Queue.bounded[IO, WebSocketFrame](100).flatMap { q => WebSocketBuilder[IO].build( send = q.dequeue, receive = q.enqueue ) }

Slide 130

Slide 130 text

BONUS SLIDE: WEBSOCKETS case GET -> Root / "ws" / "echo" => Queue.bounded[IO, WebSocketFrame](100).flatMap { q => WebSocketBuilder[IO].build( ) } send = q.dequeue, receive = q.enqueue

Slide 131

Slide 131 text

BONUS SLIDE: SWAGGER (BECAUSE SOMEONE ALWAYS ASKS)

Slide 132

Slide 132 text

BONUS SLIDE: SWAGGER (BECAUSE SOMEONE ALWAYS ASKS)