$30 off During Our Annual Pro Sale. View Details »

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. Sebastiano Poggi
    @seebrock3r
    Easy mobile backends in Kotlin
    Ktor to your heart’s content

    View Slide

  2. View Slide

  3. View Slide

  4. View Slide

  5. REST GraphQL (g)RPC

    View Slide

  6. REST GraphQL (g)RPC Firebase

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

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

    View Slide

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

    View Slide

  12. REST
    • Fully supported by Ktor

    • Pragmatic “RESTful enough”

    View Slide

  13. Ktor

    View Slide

  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

    View Slide

  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

    View Slide

  16. Ktor
    • Our example RESTful server

    • REST APIs serving JSON

    • HTML endpoints

    • Authentication

    • Running locally and deploying to AppEngine
    Docs at ktor.io

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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 {

    deploy {

    projectId = "GCLOUD_CONFIG"

    version = "GCLOUD_CONFIG"

    }

    }

    Set up Ktor and AppEngine

    View Slide

  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 {

    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

    View Slide

  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 {

    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

    View Slide

  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 {

    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

    View Slide

  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 {

    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

    View Slide

  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 {

    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

    View Slide

  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 {

    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

    View Slide

  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 {

    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

    View Slide

  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 {

    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

    View Slide

  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 {

    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

    View Slide

  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 {

    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

    View Slide

  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 {

    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

    View Slide

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

    • Ktor main configuration

    View Slide

  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

    View Slide

  34. • src/main/webapp/WEB-INF/appengine-web.xml

    • AppEngine main configuration
    Ktor and AppEngine configuration

    View Slide

  35. • 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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  40. Features
    • Modular design

    • Features

    • Pluggable pipeline

    • Everything is a feature

    • Routing, auth, content negotiation, etc.

    • Install only the features you need

    View Slide

  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

    }

    }

    }

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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 {

    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)

    }

    }

    }

    View Slide

  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

    View Slide

  49. routing {



    get("/") { }

    }
    Routing

    View Slide

  50. routing {



    get("/") { }

    post("/") { }

    put("/") { }

    delete("/") { }

    }
    Routing

    View Slide

  51. routing {



    get("/") { }

    post("/") { }

    put("/") { }

    delete("/") { }

    static("static") {

    defaultResource("404.html")

    }

    }
    Routing

    View Slide

  52. routing {

    get("/") { }

    post("/") { }

    put("/") { }

    delete("/") { }

    static("static") {

    defaultResource("404.html")

    }

    }
    Routing
    trace { logger.trace(it.buildText()) }

    View Slide

  53. routing {

    get("/") {

    }

    }
    Responding to a request

    View Slide

  54. routing {

    get("/") {

    }

    }
    Responding to a request
    :appengineRun
    :appengineDeploy
    call.respondHtml {

    head {

    title { +"PushBeat" }

    }

    body {

    h1 { +"PushBeat says hi!" }

    }

    }

    View Slide

  55. routing {

    get("/") {

    }

    }
    call.respondText(

    contentType = ContentType.Application.Json,

    text = """{ "status": "OK" }""",

    status = HttpStatusCode.OK

    )
    Responding to a request

    View Slide

  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

    )

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

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

    View Slide

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

    View Slide

  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

    View Slide

  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 {

    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

    View Slide

  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 {

    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

    View Slide

  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 {

    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

    View Slide

  70. 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)

    }

    View Slide

  71. 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'

    View Slide

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

    View Slide

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

    View Slide

  74. Thanks!

    View Slide