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

4580c218737149bf44d012a110612010?s=128

Sebastiano Poggi

June 30, 2020
Tweet

Transcript

  1. 2.
  2. 3.
  3. 4.
  4. 7.

    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
  5. 8.

    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
  6. 9.

    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
  7. 13.
  8. 14.

    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
  9. 15.

    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
  10. 16.

    Ktor • Our example RESTful server • REST APIs serving

    JSON • HTML endpoints • Authentication • Running locally and deploying to AppEngine Docs at ktor.io
  11. 17.

    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
  12. 18.

    $ 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
  13. 19.

    $ 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
  14. 20.

    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
  15. 21.

    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
  16. 22.

    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
  17. 23.

    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
  18. 24.

    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
  19. 25.

    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
  20. 26.

    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
  21. 27.

    "# 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
  22. 28.

    "# 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
  23. 29.

    "# 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
  24. 30.

    "# 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
  25. 31.

    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
  26. 33.

    • src/main/resources/application.conf • Ktor main configuration • src/main/resources/logback.xml • Logging

    configuration • Two appenders: STDOUT and CLOUD Ktor and AppEngine configuration
  27. 36.

    • 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
  28. 37.

    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
  29. 38.

    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
  30. 39.

    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
  31. 40.

    Features • Modular design • Features • Pluggable pipeline •

    Everything is a feature • Routing, auth, content negotiation, etc. • Install only the features you need
  32. 41.

    @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 } } }
  33. 42.

    @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
  34. 43.

    @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
  35. 44.

    @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
  36. 45.

    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
  37. 46.

    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
  38. 47.

    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) } } }
  39. 48.

    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
  40. 50.
  41. 51.

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

    } delete("/") { } static("static") { defaultResource("404.html") } } Routing
  42. 52.

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

    } delete("/") { } static("static") { defaultResource("404.html") } } Routing trace { logger.trace(it.buildText()) }
  43. 54.

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

    :appengineRun :appengineDeploy call.respondHtml { head { title { +"PushBeat" } } body { h1 { +"PushBeat says hi!" } } }
  44. 55.

    routing { get("/") { } } call.respondText( contentType = ContentType.Application.Json,

    text = """{ "status": "OK" }""", status = HttpStatusCode.OK ) Responding to a request
  45. 56.

    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 )
  46. 57.

    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
  47. 58.

    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
  48. 59.

    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
  49. 60.

    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
  50. 61.

    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
  51. 62.

    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
  52. 63.

    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
  53. 64.

    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. 65.

    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
  55. 66.

    @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
  56. 67.

    @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
  57. 68.

    @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
  58. 69.

    @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
  59. 70.

    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) }
  60. 71.

    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'
  61. 72.

    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
  62. 74.