Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Ktor to your heart’s content: easy mobile backends in Kotlin

Ktor to your heart’s content: easy mobile backends in Kotlin

Most Android developers are familiar with Kotlin and use it every day with great success (and joy!). But the bloodline of the language extends well beyond Android! From cross-platform development, to the desktop, to the web, to backends, Kotlin’s pedigree as a versatile language puts all us developers in a great position to leverage existing knowledge for a variety of tasks outside a mobile engineer’s comfort zone. In this session we’ll use Kotlin, Ktor and coroutines to write a backend for our splendid fictional mobile app product: PushBeat.

After learning the basics of a RESTful API, we’ll see what Ktor takes care of for us, what features may be helpful for our mobile projects, and how little we actually need to learn to be a productive backend engineer in no time!

---

The original deck (Keynote 10.0) is available here: https://www.dropbox.com/s/rhohmdi81sxah34/Ktor%20to%20your%20heart%27s%20content.key.zip?dl=0

The recording is here: https://www.youtube.com/watch?v=p8RA-3t0jGA

The sample project is here: https://github.com/rock3r/pushbeat

Sebastiano Poggi

June 30, 2020
Tweet

More Decks by Sebastiano Poggi

Other Decks in Programming

Transcript

  1. 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
  2. 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
  3. 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
  4. 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
  5. 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
  6. Ktor • Our example RESTful server • REST APIs serving

    JSON • HTML endpoints • Authentication • Running locally and deploying to AppEngine Docs at ktor.io
  7. 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
  8. $ 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
  9. $ 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
  10. 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<AppEngineStandardExtension> { deploy { projectId = "GCLOUD_CONFIG" version = "GCLOUD_CONFIG" } } Set up Ktor and AppEngine
  11. 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<AppEngineStandardExtension> { 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
  12. 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<AppEngineStandardExtension> { 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
  13. 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<AppEngineStandardExtension> { 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
  14. 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<AppEngineStandardExtension> { 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
  15. 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<AppEngineStandardExtension> { 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
  16. 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<AppEngineStandardExtension> { 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
  17. "# 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<AppEngineStandardExtension> { 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
  18. "# 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<AppEngineStandardExtension> { 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
  19. "# 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<AppEngineStandardExtension> { 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
  20. "# 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<AppEngineStandardExtension> { 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
  21. 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<AppEngineStandardExtension> { 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
  22. • src/main/resources/application.conf • Ktor main configuration • src/main/resources/logback.xml • Logging

    configuration • Two appenders: STDOUT and CLOUD Ktor and AppEngine configuration
  23. • 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
  24. 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
  25. 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
  26. 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
  27. Features • Modular design • Features • Pluggable pipeline •

    Everything is a feature • Routing, auth, content negotiation, etc. • Install only the features you need
  28. @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 } } }
  29. @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
  30. @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
  31. @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
  32. 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
  33. 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
  34. 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<AuthenticationException> { call.respondError(json, "Unauthorized", HttpStatusCode.Unauthorized) } exception<AuthorizationException> { call.respondError(json, "Forbidden", HttpStatusCode.Unauthorized) } exception<Throwable> { cause #$ logger.info("Unhandled exception: ${cause.message}", cause) val errorMessage = "Error while processing the request. ${cause.message}" call.respondError(json, errorMessage, HttpStatusCode.InternalServerError) } } }
  35. 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
  36. routing { get("/") { } post("/") { } put("/") {

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

    } delete("/") { } static("static") { defaultResource("404.html") } } Routing trace { logger.trace(it.buildText()) }
  38. routing { get("/") { } } Responding to a request

    :appengineRun :appengineDeploy call.respondHtml { head { title { +"PushBeat" } } body { h1 { +"PushBeat says hi!" } } }
  39. routing { get("/") { } } call.respondText( contentType = ContentType.Application.Json,

    text = """{ "status": "OK" }""", status = HttpStatusCode.OK ) Responding to a request
  40. 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 )
  41. 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
  42. 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
  43. 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
  44. 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
  45. 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
  46. 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
  47. 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
  48. internal fun Application.setupRouting(json: Json, registry: BeatSourcesRegistry) { routing { get<RootLocation>

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

    { call.respondHtml { renderRegisteredBeats(registry) } } get<BeatsLocation> { call.handleGetBeatSources(json, registry) } get<BeatLocation> { call.handleGetBeat(json, registry, it.id) } put<BeatLocation> { call.handlePutBeat(json, registry, it.id) } delete<BeatLocation> { call.handleDeleteBeat(json, registry, it.id) } get<BeatLocation.Refresh> { call.handleGetRefreshBeat(json, registry, it.beatLocation.id) } static("static") { resourceFolder("css") resourceFolder("js") Improving routing: locations
  50. @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
  51. @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<RootLocation> { call.respondHtml { renderRegisteredBeats(registry) } } get<BeatsLocation> { call.handleGetBeatSources(json, registry) } get<BeatLocation> { call.handleGetBeat(json, registry, it.id) } put<BeatLocation> { call.handlePutBeat(json, registry, it.id) } delete<BeatLocation> { call.handleDeleteBeat(json, registry, it.id) } get<BeatLocation.Refresh> { call.handleGetRefreshBeat(json, registry, it.beatLocation.id) } static("static") { resourceFolder("css") resourceFolder("js") Improving routing: locations
  52. @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<RootLocation> { call.respondHtml { renderRegisteredBeats(registry) } } get<BeatsLocation> { call.handleGetBeatSources(json, registry) } get<BeatLocation> { call.handleGetBeat(json, registry, it.id) } put<BeatLocation> { call.handlePutBeat(json, registry, it.id) } delete<BeatLocation> { call.handleDeleteBeat(json, registry, it.id) } get<BeatLocation.Refresh> { call.handleGetRefreshBeat(json, registry, it.beatLocation.id) } static("static") { resourceFolder("css") resourceFolder("js") Improving routing: locations
  53. @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<RootLocation> { call.respondHtml { renderRegisteredBeats(registry) } } get<BeatsLocation> { call.handleGetBeatSources(json, registry) } get<BeatLocation> { call.handleGetBeat(json, registry, it.id) } put<BeatLocation> { call.handlePutBeat(json, registry, it.id) } delete<BeatLocation> { call.handleDeleteBeat(json, registry, it.id) } get<BeatLocation.Refresh> { call.handleGetRefreshBeat(json, registry, it.beatLocation.id) } static("static") { resourceFolder("css") resourceFolder("js") Improving routing: locations
  54. internal fun Application.setupRouting(json: Json, registry: BeatSourcesRegistry) { routing { authenticate(AUTHENTICATOR_NAME)

    { } static("static") { Basic authentication get<RootLocation> { call.respondHtml { renderRegisteredBeats(registry) } } get<BeatsLocation> { call.handleGetBeatSources(json, registry) } get<BeatLocation> { call.handleGetBeat(json, registry, it.id) } put<BeatLocation> { call.handlePutBeat(json, registry, it.id) } delete<BeatLocation> { call.handleDeleteBeat(json, registry, it.id) } get<BeatLocation.Refresh> { call.handleGetRefreshBeat(json, registry, it.beatLocation.id) }
  55. al fun Application.setupRouting(json: Json, registry: BeatSourcesRegistry) { uting { authenticate(AUTHENTICATOR_NAME)

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

    { } static("static") { Basic authentication get<RootLocation> { call.respondHtml { renderRegisteredBeats(registry) } } get<BeatsLocation> { call.handleGetBeatSources(json, registry) } get<BeatLocation> { call.handleGetBeat(json, registry, it.id) } put<BeatLocation> { call.handlePutBeat(json, registry, it.id) } delete<BeatLocation> { call.handleDeleteBeat(json, registry, it.id) } get<BeatLocation.Refresh> { call.handleGetRefreshBeat(json, registry, it.beatLocation.id) } ""* Get registered beat providers (HTML) GET http:"#localhost:8080/ Accept: 'text/html' Authorization: Basic push beat