Slide 1

Slide 1 text

Sebastiano Poggi @seebrock3r Easy mobile backends in Kotlin Ktor to your heart’s content

Slide 2

Slide 2 text

No content

Slide 3

Slide 3 text

No content

Slide 4

Slide 4 text

No content

Slide 5

Slide 5 text

REST GraphQL (g)RPC

Slide 6

Slide 6 text

REST GraphQL (g)RPC Firebase

Slide 7

Slide 7 text

REST GraphQL (g)RPC • Oldest and simplest of them all • More of a style than a protocol • Not a standard, uses standards in the stack • Basic concepts: methods and resources • Most widely used of the three • Available pretty much everywhere

Slide 8

Slide 8 text

REST GraphQL (g)RPC • Graph-based API language created by Facebook • Not a standard, but has OSS specification • Served over HTTP • Single endpoint, using a query language

Slide 9

Slide 9 text

REST GraphQL (g)RPC • Remote Procedure Call protocol, by Google • Not a standard, but has OSS specification • Served over HTTP/2 • Based on protocol buffers (protobuf) • Generates type-safe client and server bindings

Slide 10

Slide 10 text

REST GraphQL (g)RPC What’s the best for you?

Slide 11

Slide 11 text

…it depends! REST GraphQL (g)RPC What’s the best for you?

Slide 12

Slide 12 text

REST • Fully supported by Ktor • Pragmatic “RESTful enough”

Slide 13

Slide 13 text

Ktor

Slide 14

Slide 14 text

Ktor • Kotlin-based framework for async clients and servers • Familiar language for Android devs • Easy async support with coroutines • Unopinionated • Pick the pieces you need and like • Testable Docs at ktor.io

Slide 15

Slide 15 text

Ktor • Covers both servers and clients • Support for Kotlin/Multiplatform (client) • Support for REST and WebSockets • Support for auth: basic, token-based (OAuth) Docs at ktor.io

Slide 16

Slide 16 text

Ktor • Our example RESTful server • REST APIs serving JSON • HTML endpoints • Authentication • Running locally and deploying to AppEngine Docs at ktor.io

Slide 17

Slide 17 text

PushBeat PushBeat You seem ver e se What about taking a short break • Fictional IoT service & Android app • Track your heartbeat when you push code • If it’s too high, it suggests you take a break Code at github.com/rock3r/pushbeat

Slide 18

Slide 18 text

$ curl https:"#sdk.cloud.google.com | bash 1 2 3 Set up AppEngine SDK 1. Install Google Cloud SDK 2. Create GCP project and enable billing

Slide 19

Slide 19 text

$ cd ~/path/to/project "$ gcloud init 5. Select your project $ gcloud auth login $ gcloud components install app-engine-java 4. Login to GCP Set up AppEngine SDK 3. Install AppEngine Java component

Slide 20

Slide 20 text

build.gradle.kts import com.google.cloud.tools.gradle.appengine.standard.AppEngineStandardExtension import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { kotlin("jvm") kotlin("plugin.serialization") version "1.3.72" war } buildscript { repositories { mavenCentral() jcenter() } dependencies { classpath("com.google.cloud.tools:appengine-gradle-plugin:2.2.0") } } "# Note: the AppEngine plugin currently does something funky when you apply it using the "# plugins DSL, so we need to use the old-school way for it to be able to work apply(plugin = "com.google.cloud.tools.appengine") configure { deploy { projectId = "GCLOUD_CONFIG" version = "GCLOUD_CONFIG" } } Set up Ktor and AppEngine

Slide 21

Slide 21 text

import com.google.cloud.tools.gradle.appengine.standard.AppEngineStandardExtension import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { kotlin("jvm") kotlin("plugin.serialization") version "1.3.72" war } buildscript { repositories { mavenCentral() jcenter() } dependencies { classpath("com.google.cloud.tools:appengine-gradle-plugin:2.2.0") } } "# Note: the AppEngine plugin currently does something funky when you apply it using the "# plugins DSL, so we need to use the old-school way for it to be able to work apply(plugin = "com.google.cloud.tools.appengine") configure { deploy { projectId = "GCLOUD_CONFIG" version = "GCLOUD_CONFIG" } } repositories { maven { url = uri("https:"#kotlin.bintray.com/ktor") } } dependencies { val kotlinVersion = rootProject.extra["kotlin_version"] val ktorVersion = rootProject.extra["ktor_version"] Set up Ktor and AppEngine build.gradle.kts

Slide 22

Slide 22 text

import com.google.cloud.tools.gradle.appengine.standard.AppEngineStandardExtension import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { kotlin("jvm") kotlin("plugin.serialization") version "1.3.72" war } buildscript { repositories { mavenCentral() jcenter() } dependencies { classpath("com.google.cloud.tools:appengine-gradle-plugin:2.2.0") } } "# Note: the AppEngine plugin currently does something funky when you apply it using the "# plugins DSL, so we need to use the old-school way for it to be able to work apply(plugin = "com.google.cloud.tools.appengine") configure { deploy { projectId = "GCLOUD_CONFIG" version = "GCLOUD_CONFIG" } } repositories { maven { url = uri("https:"#kotlin.bintray.com/ktor") } } dependencies { val kotlinVersion = rootProject.extra["kotlin_version"] val ktorVersion = rootProject.extra["ktor_version"] Set up Ktor and AppEngine build.gradle.kts

Slide 23

Slide 23 text

import com.google.cloud.tools.gradle.appengine.standard.AppEngineStandardExtension import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { kotlin("jvm") kotlin("plugin.serialization") version "1.3.72" war } buildscript { repositories { mavenCentral() jcenter() } dependencies { classpath("com.google.cloud.tools:appengine-gradle-plugin:2.2.0") } } "# Note: the AppEngine plugin currently does something funky when you apply it using the "# plugins DSL, so we need to use the old-school way for it to be able to work apply(plugin = "com.google.cloud.tools.appengine") configure { deploy { projectId = "GCLOUD_CONFIG" version = "GCLOUD_CONFIG" } } repositories { maven { url = uri("https:"#kotlin.bintray.com/ktor") } } dependencies { val kotlinVersion = rootProject.extra["kotlin_version"] val ktorVersion = rootProject.extra["ktor_version"] Set up Ktor and AppEngine build.gradle.kts

Slide 24

Slide 24 text

appengine { … } Groovy DSL: import com.google.cloud.tools.gradle.appengine.standard.AppEngineStandardExtension import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { kotlin("jvm") kotlin("plugin.serialization") version "1.3.72" war } buildscript { repositories { mavenCentral() jcenter() } dependencies { classpath("com.google.cloud.tools:appengine-gradle-plugin:2.2.0") } } "# Note: the AppEngine plugin currently does something funky when you apply it using the "# plugins DSL, so we need to use the old-school way for it to be able to work apply(plugin = "com.google.cloud.tools.appengine") configure { deploy { projectId = "GCLOUD_CONFIG" version = "GCLOUD_CONFIG" } } repositories { maven { url = uri("https:"#kotlin.bintray.com/ktor") } } dependencies { val kotlinVersion = rootProject.extra["kotlin_version"] val ktorVersion = rootProject.extra["ktor_version"] Set up Ktor and AppEngine build.gradle.kts

Slide 25

Slide 25 text

import com.google.cloud.tools.gradle.appengine.standard.AppEngineStandardExtension import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { kotlin("jvm") kotlin("plugin.serialization") version "1.3.72" war } buildscript { repositories { mavenCentral() jcenter() } dependencies { classpath("com.google.cloud.tools:appengine-gradle-plugin:2.2.0") } } "# Note: the AppEngine plugin currently does something funky when you apply it using the "# plugins DSL, so we need to use the old-school way for it to be able to work apply(plugin = "com.google.cloud.tools.appengine") configure { deploy { projectId = "GCLOUD_CONFIG" version = "GCLOUD_CONFIG" } } repositories { maven { url = uri("https:"#kotlin.bintray.com/ktor") } } dependencies { val kotlinVersion = rootProject.extra["kotlin_version"] val ktorVersion = rootProject.extra["ktor_version"] Set up Ktor and AppEngine cloud.google.com/sdk/install build.gradle.kts

Slide 26

Slide 26 text

buildscript { repositories { mavenCentral() jcenter() } dependencies { classpath("com.google.cloud.tools:appengine-gradle-plugin:2.2.0") } } "# Note: the AppEngine plugin currently does something funky when you apply it using the "# plugins DSL, so we need to use the old-school way for it to be able to work apply(plugin = "com.google.cloud.tools.appengine") configure { deploy { projectId = "GCLOUD_CONFIG" version = "GCLOUD_CONFIG" } } repositories { maven { url = uri("https:"#kotlin.bintray.com/ktor") } } dependencies { val kotlinVersion = rootProject.extra["kotlin_version"] val ktorVersion = rootProject.extra["ktor_version"] implementation(kotlin("stdlib-jdk8")) implementation("ch.qos.logback:logback-classic:1.2.3") implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlinVersion") implementation("org.jetbrains.kotlinx:kotlinx-serialization-runtime:0.20.0") implementation("io.ktor:ktor-server-servlet:$ktorVersion") implementation("io.ktor:ktor-html-builder:$ktorVersion") implementation("io.ktor:ktor-auth:$ktorVersion") Set up Ktor and AppEngine build.gradle.kts

Slide 27

Slide 27 text

"# Note: the AppEngine plugin currently does something funky when you apply it using the "# plugins DSL, so we need to use the old-school way for it to be able to work apply(plugin = "com.google.cloud.tools.appengine") configure { deploy { projectId = "GCLOUD_CONFIG" version = "GCLOUD_CONFIG" } } repositories { maven { url = uri("https:"#kotlin.bintray.com/ktor") } } dependencies { val kotlinVersion = rootProject.extra["kotlin_version"] val ktorVersion = rootProject.extra["ktor_version"] implementation(kotlin("stdlib-jdk8")) implementation("ch.qos.logback:logback-classic:1.2.3") implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlinVersion") implementation("org.jetbrains.kotlinx:kotlinx-serialization-runtime:0.20.0") implementation("io.ktor:ktor-server-servlet:$ktorVersion") implementation("io.ktor:ktor-html-builder:$ktorVersion") implementation("io.ktor:ktor-auth:$ktorVersion") implementation("io.ktor:ktor-locations:$ktorVersion") implementation("io.ktor:ktor-serialization:$ktorVersion") implementation("io.ktor:ktor-server-host-common:$ktorVersion") providedCompile("com.google.appengine:appengine:1.9.60") testImplementation("io.ktor:ktor-server-tests:$ktorVersion") } Set up Ktor and AppEngine build.gradle.kts

Slide 28

Slide 28 text

"# Note: the AppEngine plugin currently does something funky when you apply it using the "# plugins DSL, so we need to use the old-school way for it to be able to work apply(plugin = "com.google.cloud.tools.appengine") configure { deploy { projectId = "GCLOUD_CONFIG" version = "GCLOUD_CONFIG" } } repositories { maven { url = uri("https:"#kotlin.bintray.com/ktor") } } dependencies { val kotlinVersion = rootProject.extra["kotlin_version"] val ktorVersion = rootProject.extra["ktor_version"] implementation(kotlin("stdlib-jdk8")) implementation("ch.qos.logback:logback-classic:1.2.3") implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlinVersion") implementation("org.jetbrains.kotlinx:kotlinx-serialization-runtime:0.20.0") implementation("io.ktor:ktor-server-servlet:$ktorVersion") implementation("io.ktor:ktor-html-builder:$ktorVersion") implementation("io.ktor:ktor-auth:$ktorVersion") implementation("io.ktor:ktor-locations:$ktorVersion") implementation("io.ktor:ktor-serialization:$ktorVersion") implementation("io.ktor:ktor-server-host-common:$ktorVersion") providedCompile("com.google.appengine:appengine:1.9.60") testImplementation("io.ktor:ktor-server-tests:$ktorVersion") } Set up Ktor and AppEngine Features build.gradle.kts

Slide 29

Slide 29 text

"# Note: the AppEngine plugin currently does something funky when you apply it using the "# plugins DSL, so we need to use the old-school way for it to be able to work apply(plugin = "com.google.cloud.tools.appengine") configure { deploy { projectId = "GCLOUD_CONFIG" version = "GCLOUD_CONFIG" } } repositories { maven { url = uri("https:"#kotlin.bintray.com/ktor") } } dependencies { val kotlinVersion = rootProject.extra["kotlin_version"] val ktorVersion = rootProject.extra["ktor_version"] implementation(kotlin("stdlib-jdk8")) implementation("ch.qos.logback:logback-classic:1.2.3") implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlinVersion") implementation("org.jetbrains.kotlinx:kotlinx-serialization-runtime:0.20.0") implementation("io.ktor:ktor-server-servlet:$ktorVersion") implementation("io.ktor:ktor-html-builder:$ktorVersion") implementation("io.ktor:ktor-auth:$ktorVersion") implementation("io.ktor:ktor-locations:$ktorVersion") implementation("io.ktor:ktor-serialization:$ktorVersion") implementation("io.ktor:ktor-server-host-common:$ktorVersion") providedCompile("com.google.appengine:appengine:1.9.60") testImplementation("io.ktor:ktor-server-tests:$ktorVersion") } Set up Ktor and AppEngine build.gradle.kts

Slide 30

Slide 30 text

"# Note: the AppEngine plugin currently does something funky when you apply it using the "# plugins DSL, so we need to use the old-school way for it to be able to work apply(plugin = "com.google.cloud.tools.appengine") configure { deploy { projectId = "GCLOUD_CONFIG" version = "GCLOUD_CONFIG" } } repositories { maven { url = uri("https:"#kotlin.bintray.com/ktor") } } dependencies { val kotlinVersion = rootProject.extra["kotlin_version"] val ktorVersion = rootProject.extra["ktor_version"] implementation(kotlin("stdlib-jdk8")) implementation("ch.qos.logback:logback-classic:1.2.3") implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlinVersion") implementation("org.jetbrains.kotlinx:kotlinx-serialization-runtime:0.20.0") implementation("io.ktor:ktor-server-servlet:$ktorVersion") implementation("io.ktor:ktor-html-builder:$ktorVersion") implementation("io.ktor:ktor-auth:$ktorVersion") implementation("io.ktor:ktor-locations:$ktorVersion") implementation("io.ktor:ktor-serialization:$ktorVersion") implementation("io.ktor:ktor-server-host-common:$ktorVersion") providedCompile("com.google.appengine:appengine:1.9.60") testImplementation("io.ktor:ktor-server-tests:$ktorVersion") } Set up Ktor and AppEngine build.gradle.kts

Slide 31

Slide 31 text

build.gradle.kts "# Note: the AppEngine plugin currently does something funky when you apply it using the "# plugins DSL, so we need to use the old-school way for it to be able to work apply(plugin = "com.google.cloud.tools.appengine") configure { deploy { projectId = "GCLOUD_CONFIG" version = "GCLOUD_CONFIG" } } repositories { maven { url = uri("https:"#kotlin.bintray.com/ktor") } } dependencies { val kotlinVersion = rootProject.extra["kotlin_version"] val ktorVersion = rootProject.extra["ktor_version"] implementation(kotlin("stdlib-jdk8")) implementation("ch.qos.logback:logback-classic:1.2.3") implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlinVersion") implementation("org.jetbrains.kotlinx:kotlinx-serialization-runtime:0.20.0") implementation("io.ktor:ktor-server-servlet:$ktorVersion") implementation("io.ktor:ktor-html-builder:$ktorVersion") implementation("io.ktor:ktor-auth:$ktorVersion") implementation("io.ktor:ktor-locations:$ktorVersion") implementation("io.ktor:ktor-serialization:$ktorVersion") implementation("io.ktor:ktor-server-host-common:$ktorVersion") providedCompile("com.google.appengine:appengine:1.9.60") testImplementation("io.ktor:ktor-server-tests:$ktorVersion") } Set up Ktor and AppEngine

Slide 32

Slide 32 text

Ktor and AppEngine configuration • src/main/resources/application.conf • Ktor main configuration

Slide 33

Slide 33 text

• src/main/resources/application.conf • Ktor main configuration • src/main/resources/logback.xml • Logging configuration • Two appenders: STDOUT and CLOUD Ktor and AppEngine configuration

Slide 34

Slide 34 text

• src/main/webapp/WEB-INF/appengine-web.xml • AppEngine main configuration Ktor and AppEngine configuration

Slide 35

Slide 35 text

• src/main/webapp/WEB-INF/appengine-web.xml • AppEngine main configuration • src/main/webapp/WEB-INF/logging.properties • Cloud logging configuration Ktor and AppEngine configuration

Slide 36

Slide 36 text

• src/main/webapp/WEB-INF/appengine-web.xml • AppEngine main configuration • src/main/webapp/WEB-INF/logging.properties • Cloud logging configuration • src/main/webapp/WEB-INF/web.xml • Servlet configuration Ktor and AppEngine configuration

Slide 37

Slide 37 text

package dev.sebastiano.pushbeat.api @Suppress(“unused") "# Referenced in application.conf fun Application.module() { "# ""& } • In Application.kt ktor { deployment { port = 8080 port = ${?PORT} } application { modules = [ dev.sebastiano.pushbeat.api.ApplicationKt.module ] } } • What to start is defined in application.conf Start your server up

Slide 38

Slide 38 text

package dev.sebastiano.pushbeat.api @Suppress(“unused") "# Referenced in application.conf fun Application.module() { "# ""& } • In Application.kt ktor { deployment { port = 8080 port = ${?PORT} } application { modules = [ dev.sebastiano.pushbeat.api.ApplicationKt.module ] } } • What to start is defined in application.conf Start your server up

Slide 39

Slide 39 text

package dev.sebastiano.pushbeat.api @Suppress(“unused") "# Referenced in application.conf fun Application.module() { "# ""& } • In Application.kt ktor { deployment { port = 8080 port = ${?PORT} } application { modules = [ dev.sebastiano.pushbeat.api.ApplicationKt.module ] } } • What to start is defined in application.conf Start your server up

Slide 40

Slide 40 text

Features • Modular design • Features • Pluggable pipeline • Everything is a feature • Routing, auth, content negotiation, etc. • Install only the features you need

Slide 41

Slide 41 text

@Suppress("unused") "# Referenced in application.conf @JvmOverloads fun Application.module(testing: Boolean = false) { setupAuthentication() install(Locations) { } @Suppress("MagicNumber") "# Just one-off configuration install(Compression) { gzip() deflate { priority = 10.0 minimumSize(1024) "# condition } } install(DefaultHeaders) { header("X-Engine", "Ktor") "# will send this header with each response } val json = createJsonInstance() install(ContentNegotiation) { json(json) } install(CallLogging) { level = Level.INFO Features install(Authentication) { basic(AUTHENTICATOR_NAME) { realm = "PushBeat" validate { if (it.name "' "push" "$ it.password "' “beat”) { UserIdPrincipal(it.name) } else null } } }

Slide 42

Slide 42 text

@Suppress("unused") "# Referenced in application.conf @JvmOverloads fun Application.module(testing: Boolean = false) { setupAuthentication() install(Locations) { } @Suppress("MagicNumber") "# Just one-off configuration install(Compression) { gzip() deflate { priority = 10.0 minimumSize(1024) "# condition } } install(DefaultHeaders) { header("X-Engine", "Ktor") "# will send this header with each response } val json = createJsonInstance() install(ContentNegotiation) { json(json) } install(CallLogging) { level = Level.INFO Features

Slide 43

Slide 43 text

@Suppress("unused") "# Referenced in application.conf @JvmOverloads fun Application.module(testing: Boolean = false) { setupAuthentication() install(Locations) { } @Suppress("MagicNumber") "# Just one-off configuration install(Compression) { gzip() deflate { priority = 10.0 minimumSize(1024) "# condition } } install(DefaultHeaders) { header("X-Engine", "Ktor") "# will send this header with each response } val json = createJsonInstance() install(ContentNegotiation) { json(json) } install(CallLogging) { level = Level.INFO Features

Slide 44

Slide 44 text

@Suppress("unused") "# Referenced in application.conf @JvmOverloads fun Application.module(testing: Boolean = false) { setupAuthentication() install(Locations) { } @Suppress("MagicNumber") "# Just one-off configuration install(Compression) { gzip() deflate { priority = 10.0 minimumSize(1024) "# condition } } install(DefaultHeaders) { header("X-Engine", "Ktor") "# will send this header with each response } val json = createJsonInstance() install(ContentNegotiation) { json(json) } install(CallLogging) { level = Level.INFO Features

Slide 45

Slide 45 text

install(Locations) { } @Suppress("MagicNumber") "# Just one-off configuration install(Compression) { gzip() deflate { priority = 10.0 minimumSize(1024) "# condition } } install(DefaultHeaders) { header("X-Engine", "Ktor") "# will send this header with each response } val json = createJsonInstance() install(ContentNegotiation) { json(json) } install(CallLogging) { level = Level.INFO format { call #$ when (val status = call.response.status() "( "Unhandled") { HttpStatusCode.Found ") "$status: ${call.request.httpMethod.value} - " + "${call.request.uri} [accept: '${call.request.accept()}'] ") " + "${call.response.headers[HttpHeaders.Location]}" else ") "$status: ${call.request.httpMethod.value} - ${call.request.uri}" + "[accept: '${call.request.accept()}']" } } } setupStatusPages(json, logger) setupRouting(json, BeatSourcesRegistry) } Features

Slide 46

Slide 46 text

install(Locations) { } @Suppress("MagicNumber") "# Just one-off configuration install(Compression) { gzip() deflate { priority = 10.0 minimumSize(1024) "# condition } } install(DefaultHeaders) { header("X-Engine", "Ktor") "# will send this header with each response } val json = createJsonInstance() install(ContentNegotiation) { json(json) } install(CallLogging) { level = Level.INFO format { call #$ when (val status = call.response.status() "( "Unhandled") { HttpStatusCode.Found ") "$status: ${call.request.httpMethod.value} - " + "${call.request.uri} [accept: '${call.request.accept()}'] ") " + "${call.response.headers[HttpHeaders.Location]}" else ") "$status: ${call.request.httpMethod.value} - ${call.request.uri}" + "[accept: '${call.request.accept()}']" } } } setupStatusPages(json, logger) setupRouting(json, BeatSourcesRegistry) } Features

Slide 47

Slide 47 text

install(Locations) { } @Suppress("MagicNumber") "# Just one-off configuration install(Compression) { gzip() deflate { priority = 10.0 minimumSize(1024) "# condition } } install(DefaultHeaders) { header("X-Engine", "Ktor") "# will send this header with each response } val json = createJsonInstance() install(ContentNegotiation) { json(json) } install(CallLogging) { level = Level.INFO format { call #$ when (val status = call.response.status() "( "Unhandled") { HttpStatusCode.Found ") "$status: ${call.request.httpMethod.value} - " + "${call.request.uri} [accept: '${call.request.accept()}'] ") " + "${call.response.headers[HttpHeaders.Location]}" else ") "$status: ${call.request.httpMethod.value} - ${call.request.uri}" + "[accept: '${call.request.accept()}']" } } } setupStatusPages(json, logger) setupRouting(json, BeatSourcesRegistry) } Features internal fun Application.setupStatusPages(json: Json, logger: Logger) { install(StatusPages) { status(HttpStatusCode.NotFound) { statusCode #$ val message = generate404ErrorMessage(call.request.uri) call.respondError(json, message, statusCode) } exception { call.respondError(json, "Unauthorized", HttpStatusCode.Unauthorized) } exception { call.respondError(json, "Forbidden", HttpStatusCode.Unauthorized) } exception { cause #$ logger.info("Unhandled exception: ${cause.message}", cause) val errorMessage = "Error while processing the request. ${cause.message}" call.respondError(json, errorMessage, HttpStatusCode.InternalServerError) } } }

Slide 48

Slide 48 text

install(Locations) { } @Suppress("MagicNumber") "# Just one-off configuration install(Compression) { gzip() deflate { priority = 10.0 minimumSize(1024) "# condition } } install(DefaultHeaders) { header("X-Engine", "Ktor") "# will send this header with each response } val json = createJsonInstance() install(ContentNegotiation) { json(json) } install(CallLogging) { level = Level.INFO format { call #$ when (val status = call.response.status() "( "Unhandled") { HttpStatusCode.Found ") "$status: ${call.request.httpMethod.value} - " + "${call.request.uri} [accept: '${call.request.accept()}'] ") " + "${call.response.headers[HttpHeaders.Location]}" else ") "$status: ${call.request.httpMethod.value} - ${call.request.uri}" + "[accept: '${call.request.accept()}']" } } } setupStatusPages(json, logger) setupRouting(json, BeatSourcesRegistry) } Features

Slide 49

Slide 49 text

routing { get("/") { } } Routing

Slide 50

Slide 50 text

routing { get("/") { } post("/") { } put("/") { } delete("/") { } } Routing

Slide 51

Slide 51 text

routing { get("/") { } post("/") { } put("/") { } delete("/") { } static("static") { defaultResource("404.html") } } Routing

Slide 52

Slide 52 text

routing { get("/") { } post("/") { } put("/") { } delete("/") { } static("static") { defaultResource("404.html") } } Routing trace { logger.trace(it.buildText()) }

Slide 53

Slide 53 text

routing { get("/") { } } Responding to a request

Slide 54

Slide 54 text

routing { get("/") { } } Responding to a request :appengineRun :appengineDeploy call.respondHtml { head { title { +"PushBeat" } } body { h1 { +"PushBeat says hi!" } } }

Slide 55

Slide 55 text

routing { get("/") { } } call.respondText( contentType = ContentType.Application.Json, text = """{ "status": "OK" }""", status = HttpStatusCode.OK ) Responding to a request

Slide 56

Slide 56 text

routing { } Responding to a request get("/{id}") { val id = call.parameters["id"] val name = call.request.queryParameters["name"] call.respondText( contentType ="""{ "id": "$id", "name": "$name" }""", contentType = ContentType.Application.Json )

Slide 57

Slide 57 text

internal fun Application.setupRouting(json: Json, registry: BeatSourcesRegistry) { routing { get("/") { call.respondHtml { renderRegisteredBeats(registry) } } get("/beats") { call.handleGetBeatSources(json, registry) } get("/beat/{id}") { call.handleGetBeat(json, registry) } put("/beat/{id}") { call.handlePutBeat(json, registry) } delete("/beat/{id}") { call.handleDeleteBeat(json, registry) } get("/beat/{id}/refresh") { call.handleGetRefreshBeat(json, registry, it.beatLocation.id) } static("static") { resourceFolder("css") resourceFolder("js") Routing in PushBeat

Slide 58

Slide 58 text

internal fun HTML.renderRegisteredBeats(registry: BeatSourcesRegistry) { head { title { +"PushBeat" } styleLink("/static/css/pushbeat.css") script(src = "/static/js/pushbeat.js", type = LinkType.textJavaScript) {} } body { h1 { +"PushBeat" } h2 { +"Registered beats" } ul { val sources = registry.sources() if (sources.isEmpty()) { li { i { +"None" } } return@ul } for (source in sources) { li { p { b { +"${source.name} " span(classes = "secondary") { +"(id: " code { +source.id } +")" } } br Routing in PushBeat

Slide 59

Slide 59 text

internal fun HTML.renderRegisteredBeats(registry: BeatSourcesRegistry) { head { title { +"PushBeat" } styleLink("/static/css/pushbeat.css") script(src = "/static/js/pushbeat.js", type = LinkType.textJavaScript) {} } body { h1 { +"PushBeat" } h2 { +"Registered beats" } ul { val sources = registry.sources() if (sources.isEmpty()) { li { i { +"None" } } return@ul } for (source in sources) { li { p { b { +"${source.name} " span(classes = "secondary") { +"(id: " code { +source.id } +")" } } br +"Last BPM: ${source.beats.valueOrNull "( "N/A"}" span(classes = "refresh") { onClick = "javascript:refreshBeatValue('${source.id}'); location.reload();" +"refresh" } } } Routing in PushBeat

Slide 60

Slide 60 text

internal fun HTML.renderRegisteredBeats(registry: BeatSourcesRegistry) { head { title { +"PushBeat" } styleLink("/static/css/pushbeat.css") script(src = "/static/js/pushbeat.js", type = LinkType.textJavaScript) {} } body { h1 { +"PushBeat" } h2 { +"Registered beats" } ul { val sources = registry.sources() if (sources.isEmpty()) { li { i { +"None" } } return@ul } for (source in sources) { li { p { b { +"${source.name} " span(classes = "secondary") { +"(id: " code { +source.id } +")" } } br +"Last BPM: ${source.beats.valueOrNull "( "N/A"}" span(classes = "refresh") { onClick = "javascript:refreshBeatValue('${source.id}'); location.reload();" +"refresh" } } } } } } } Routing in PushBeat

Slide 61

Slide 61 text

title { +"PushBeat" } styleLink("/static/css/pushbeat.css") script(src = "/static/js/pushbeat.js", type = LinkType.textJavaScript) {} } body { h1 { +"PushBeat" } h2 { +"Registered beats" } ul { val sources = registry.sources() if (sources.isEmpty()) { li { i { +"None" } } return@ul } for (source in sources) { li { p { b { +"${source.name} " span(classes = "secondary") { +"(id: " code { +source.id } +")" } } br +"Last BPM: ${source.beats.valueOrNull "( "N/A"}" span(classes = "refresh") { onClick = "javascript:refreshBeatValue('${source.id}'); location.reload();" +"refresh" } } } } } } } Routing in PushBeat

Slide 62

Slide 62 text

internal fun Application.setupRouting(json: Json, registry: BeatSourcesRegistry) { routing { get("/") { call.respondHtml { renderRegisteredBeats(registry) } } get("/beats") { call.handleGetBeatSources(json, registry) } get("/beat/{id}") { call.handleGetBeat(json, registry) } put("/beat/{id}") { call.handlePutBeat(json, registry) } delete("/beat/{id}") { call.handleDeleteBeat(json, registry) } get("/beat/{id}/refresh") { call.handleGetRefreshBeat(json, registry, it.beatLocation.id) } static("static") { resourceFolder("css") resourceFolder("js") defaultResource("404.html") } } } private fun Route.resourceFolder(name: String) { static(name) { resources(name) } } Routing in PushBeat

Slide 63

Slide 63 text

internal fun Application.setupRouting(json: Json, registry: BeatSourcesRegistry) { routing { get("/") { call.respondHtml { renderRegisteredBeats(registry) } } get("/beats") { call.handleGetBeatSources(json, registry) } get("/beat/{id}") { call.handleGetBeat(json, registry) } put("/beat/{id}") { call.handlePutBeat(json, registry) } delete("/beat/{id}") { call.handleDeleteBeat(json, registry) } get("/beat/{id}/refresh") { call.handleGetRefreshBeat(json, registry) } static("static") { resourceFolder("css") resourceFolder("js") Improving routing: locations

Slide 64

Slide 64 text

internal fun Application.setupRouting(json: Json, registry: BeatSourcesRegistry) { routing { get { call.respondHtml { renderRegisteredBeats(registry) } } get { call.handleGetBeatSources(json, registry) } get { call.handleGetBeat(json, registry, it.id) } put { call.handlePutBeat(json, registry, it.id) } delete { call.handleDeleteBeat(json, registry, it.id) } get { call.handleGetRefreshBeat(json, registry, it.beatLocation.id) } static("static") { resourceFolder("css") resourceFolder("js") Improving routing: locations

Slide 65

Slide 65 text

internal fun Application.setupRouting(json: Json, registry: BeatSourcesRegistry) { routing { get { call.respondHtml { renderRegisteredBeats(registry) } } get { call.handleGetBeatSources(json, registry) } get { call.handleGetBeat(json, registry, it.id) } put { call.handlePutBeat(json, registry, it.id) } delete { call.handleDeleteBeat(json, registry, it.id) } get { call.handleGetRefreshBeat(json, registry, it.beatLocation.id) } static("static") { resourceFolder("css") resourceFolder("js") Improving routing: locations

Slide 66

Slide 66 text

@Location("/") class RootLocation @Location("/beats") class BeatsLocation @Location("/beat/{id}") class BeatLocation(val id: String) { @Location("/refresh") class Refresh(val beatLocation: BeatLocation) } SourcesRegistry) { ation.id) Improving routing: locations

Slide 67

Slide 67 text

@Location("/beat/{id}") class BeatLocation(val id: String) { @Location("/refresh") class Refresh( val beatLocation: BeatLocation ) } internal fun Application.setupRouting(json: Json, registry: BeatSourcesRegistry) { routing { get { call.respondHtml { renderRegisteredBeats(registry) } } get { call.handleGetBeatSources(json, registry) } get { call.handleGetBeat(json, registry, it.id) } put { call.handlePutBeat(json, registry, it.id) } delete { call.handleDeleteBeat(json, registry, it.id) } get { call.handleGetRefreshBeat(json, registry, it.beatLocation.id) } static("static") { resourceFolder("css") resourceFolder("js") Improving routing: locations

Slide 68

Slide 68 text

@Location("/beat/{id}") class BeatLocation(val id: String) { @Location("/refresh") class Refresh( val beatLocation: BeatLocation ) } internal fun Application.setupRouting(json: Json, registry: BeatSourcesRegistry) { routing { get { call.respondHtml { renderRegisteredBeats(registry) } } get { call.handleGetBeatSources(json, registry) } get { call.handleGetBeat(json, registry, it.id) } put { call.handlePutBeat(json, registry, it.id) } delete { call.handleDeleteBeat(json, registry, it.id) } get { call.handleGetRefreshBeat(json, registry, it.beatLocation.id) } static("static") { resourceFolder("css") resourceFolder("js") Improving routing: locations

Slide 69

Slide 69 text

@Location("/beat/{id}") class BeatLocation(val id: String) { @Location("/refresh") class Refresh( val beatLocation: BeatLocation ) } internal fun Application.setupRouting(json: Json, registry: BeatSourcesRegistry) { routing { get { call.respondHtml { renderRegisteredBeats(registry) } } get { call.handleGetBeatSources(json, registry) } get { call.handleGetBeat(json, registry, it.id) } put { call.handlePutBeat(json, registry, it.id) } delete { call.handleDeleteBeat(json, registry, it.id) } get { call.handleGetRefreshBeat(json, registry, it.beatLocation.id) } static("static") { resourceFolder("css") resourceFolder("js") Improving routing: locations

Slide 70

Slide 70 text

internal fun Application.setupRouting(json: Json, registry: BeatSourcesRegistry) { routing { authenticate(AUTHENTICATOR_NAME) { } static("static") { Basic authentication get { call.respondHtml { renderRegisteredBeats(registry) } } get { call.handleGetBeatSources(json, registry) } get { call.handleGetBeat(json, registry, it.id) } put { call.handlePutBeat(json, registry, it.id) } delete { call.handleDeleteBeat(json, registry, it.id) } get { call.handleGetRefreshBeat(json, registry, it.beatLocation.id) }

Slide 71

Slide 71 text

al fun Application.setupRouting(json: Json, registry: BeatSourcesRegistry) { uting { authenticate(AUTHENTICATOR_NAME) { } static("static") { Basic authentication get { call.respondHtml { renderRegisteredBeats(registry) } } get { call.handleGetBeatSources(json, registry) } get { call.handleGetBeat(json, registry, it.id) } put { call.handlePutBeat(json, registry, it.id) } delete { call.handleDeleteBeat(json, registry, it.id) } get { call.handleGetRefreshBeat(json, registry, it.beatLocation.id) } ""* Get registered beat providers (HTML) GET http:"#localhost:8080/ Accept: 'text/html'

Slide 72

Slide 72 text

al fun Application.setupRouting(json: Json, registry: BeatSourcesRegistry) { uting { authenticate(AUTHENTICATOR_NAME) { } static("static") { Basic authentication get { call.respondHtml { renderRegisteredBeats(registry) } } get { call.handleGetBeatSources(json, registry) } get { call.handleGetBeat(json, registry, it.id) } put { call.handlePutBeat(json, registry, it.id) } delete { call.handleDeleteBeat(json, registry, it.id) } get { call.handleGetRefreshBeat(json, registry, it.beatLocation.id) } ""* Get registered beat providers (HTML) GET http:"#localhost:8080/ Accept: 'text/html' Authorization: Basic push beat

Slide 73

Slide 73 text

Image credits Some images designed by Freepik / rawpixel.com, macrovector; octicons Question time!

Slide 74

Slide 74 text

Thanks!