Pro Kotlin Web Apps From Scratch Building Producti...
Pro Kotlin Web Apps From Scratch Building Producti...
This work is subject to copyright. All rights are solely and exclusively
licensed by the Publisher, whether the whole or part of the material is
concerned, specifically the rights of translation, reprinting, reuse of
illustrations, recitation, broadcasting, reproduction on microfilms or in any
other physical way, and transmission or information storage and retrieval,
electronic adaptation, computer software, or by similar or dissimilar
methodology now known or hereafter developed.
The publisher, the authors, and the editors are safe to assume that the advice
and information in this book are believed to be true and accurate at the date
of publication. Neither the publisher nor the authors or the editors give a
warranty, expressed or implied, with respect to the material contained
herein or for any errors or omissions that may have been made. The
publisher remains neutral with regard to jurisdictional claims in published
maps and institutional affiliations.
Figure 1-1 Download IntelliJ IDEA Community Edition for your platform
For more details on how to install and run IntelliJ IDEA, refer to the
JetBrains website. For example, the download page has a link with
installation instructions in the left sidebar, as seen in Figure 1-1.
Click “New Project” to create a new Kotlin project using the built-in
wizard in IntelliJ IDEA, shown in Figure 1-3.
Figure 1-3 The New Project screen in IntelliJ IDEA
Use your good taste and judgment to name the project, and adjust the
options as follows:
Make sure you select “New Project” in the left sidebar.
Set Language to Kotlin.
Set Build system to Gradle.
Set Gradle DSL to Kotlin.
Uncheck Add sample code.
You need a JDK, and the easiest way to get one is to have IntelliJ IDEA
download one for you. Click the red “<No SDK>” dropdown in the New
Project dialog shown in Figure 1-3 and choose an appropriate JDK from the
popup that appears, shown in Figure 1-4. JDK 17 or JDK 11 is a good
choice, as those are stable Long-Term Support (LTS) releases.
Figure 1-4 Use IntelliJ IDEA to download a JDK
There are multiple vendors available. They are all full-fledged JDKs
that adhere to the JDK specifications. For the most part, it doesn’t matter
which one you choose. The differences lie in a handful of edge cases and
different defaults. For most web apps, you’ll be able to switch between
vendors and not notice any differences. I personally prefer Amazon
Corretto, mostly out of habit. But I’ve used other vendors in real-world web
apps, such as Azul Zulu. I tend to choose whichever vendor is most
convenient on the platform I use, such as Azul Zulu on Azure, Amazon
Corretto on AWS, and so on.
Make sure you choose a JDK of version 11 or newer. Versions 11 and
17 are both Long-Time Support (LTS) releases, which means they’ll receive
security updates longer than other versions. And later in this book, you’ll
use libraries that require at least version 11 to work.
You can also choose an existing JDK if you have one installed.
Alternatively, you can manage JDK installations yourself, by using
something like sdkman (https://sdkman.io/) on macOS and Linux
and Jabba (https://github.com/shyiko/jabba) on Windows.
Tip You can have bad luck and choose a JDK version that’s
incompatible with the Gradle version used by IntelliJ IDEA. For
example, the 2021 version of IntelliJ uses Gradle 7.1, which only works
with JDK versions lower than 16. If this happens, delete your project and
start over, using a different JDK or Gradle version.
After IntelliJ IDEA finishes downloading the JDK, click the “Create”
button at the bottom of the New Project dialog shown in Figure 1-3, and
your brand-new project appears, as shown in Figure 1-5.
Figure 1-5 IntelliJ IDEA after it has finished with project initialization
With this setup, IntelliJ IDEA won’t create sample code or pre-
generated files, other than a Gradle build system skeleton. All you have
now is a way to compile and run Kotlin code. And that’s all you need in this
book. There’s no framework with tens of thousands of lines of code that
needs to run first to get anything done.
Tip Gradle is a build system for the Java platform. This is where you
add third-party code as dependencies, configure production builds, set up
automated testing, and more. Maven is also an excellent choice for
building production-grade Kotlin web apps. Most examples you’ll see in
books and online documentation use Gradle, though, and I’ve only used
Gradle in real-world Kotlin apps, so that’s why I use it in this book.
Kotlin Hello, World!
It’s common when learning a new language to make one’s first program to
be one that outputs “Hello, World!” This ensures that everything is up and
running, that you can compile Kotlin code, and that you can run it and see
that it outputs something.
package kotlinbook
fun main() {
println("Hello, World!")
}
Listing 1-1 Print “Hello, World!” in src/main/kotlinbook/Main.kt
Figure 1-7 A green play button next to the function name in IntelliJ IDEA shows a runnable
function
Run Your Code with Gradle
You can also configure Gradle to run your code. Later, you’ll use Gradle to
build deployable JAR (Java archive) files, and Gradle needs to know where
your main function is to be able to build them. Additionally, even if you’re
not going to run the code with Gradle, developers unfamiliar with the
project can look at the Gradle config to find the main function. This helps
understand where the main entry point is in the project and is a good place
to start reading code to become familiar with it.
You’ll use the application plugin in Gradle to specify a runnable
application with a main entry point. In the file build.gradle.kts, add the
application plugin:
plugins {
kotlin("jvm") version "..."
application
}
application {
mainClass.set("kotlinbook.MainKt")
}
Tip Did you notice the leaky abstraction? The application plugin
is not aware of Kotlin, and it needs to know the name of the compiled
Java class file. To be compatible and interoperable with the Java
platform at large, the Kotlin compiler turns Main into MainKt. A file
Foo.kt that only declares the class Foo is compiled to Foo.class. Any
other scenario will generate FooKt.class.
To run the function with Gradle, use either of the two following methods:
In a terminal (either stand-alone or the Terminal tab at the bottom of
IntelliJ), type ./gradlew application.
In the Gradle panel in IntelliJ (found in the rightmost sidebar), click
Tasks ➤ application ➤ run.
You should now see the output of your application, either in your
terminal or in the IntelliJ “Run” output panel.
When you see this progress bar, you should wait until it’s gone before
you start editing code.
IntelliJ IDEA knows a lot about your code. To gain that knowledge, it
needs to analyze your code first. When you type in new code, this will
happen automatically (and so quick you probably won’t notice). But when
you create a new project or open an existing project for the first time,
IntelliJ IDEA might spend many minutes running analysis in the
background.
The editor is still active while analysis is running, so you assume that
IntelliJ IDEA is ready to go. But many core features won’t be available
until the analysis is complete.
When the progress bar is gone, IntelliJ IDEA is ready to go, and you
can now edit code with all the features IntelliJ IDEA provides.
The icon will only be visible when there are changes made to
build.gradle.kts that IntelliJ IDEA is not yet aware of. But it’s easy to miss,
so make sure you refresh your Gradle project in IntelliJ IDEA every time
you change build.gradle.kts.
Figure 1-10 Partially typing LocalTime in IntelliJ IDEA and getting auto-completion
Figure 1-11 Hit the Enter key, and the auto-completion will add the full class name and add an
import automatically
This chapter will give you superpowers. You will get all the way to a
working web app with routing, views/templates, and logging. You’ll set the
stage for what’s to come in later chapters. It will just be you and your IDE,
no starter kits or frameworks.
If you’re new to Kotlin, here are some language features you’ll see
examples of in this chapter:
Lambdas
Named arguments
Also in this chapter, you will learn the following about creating web
apps:
Setting up a web server, powered by Ktor, listening on port 4207
Logging setup, using SLF4J and Logback, configurable with standard
logback.xml setup
Tweaking Ktor to display useful error messages when your application
throws unexpected errors
You’ll wire up all these things yourself, instead of having a framework
do it for you. If you’ve only used frameworks before, I envy you. Prepare to
be surprised how little manual wiring you need to do to get it done.
Web Server Hello, World!
In Chapter 1, you wrote a small program that printed “Hello, World!” It’s
time to take it to the next level: a full-fledged working web app that starts a
server, sets up URL routing, and outputs “Hello, World!” over HTTP.
Choosing Ktor
You need a library to handle URL routing and to start up a web server to
host your web app. In this book, you’ll use Ktor. You can swap out Ktor for
something else later if you want, and in Appendix A you’ll learn how to use
Jooby instead of Ktor. But in the rest of the book, all the code you’ll write
will use Ktor.
Ktor is one of the most popular libraries for web app routing in Kotlin.
It has everything you need to build production-grade web apps, and it’s
written in Kotlin, so it’s both convenient and powerful to use and makes use
of all the relevant features that Kotlin has to offer for building web apps.
It’s also built to be highly scalable and performant and is battle proven
across a large number of real-world web apps.
And, most importantly, Ktor is a library, not a framework. Frameworks,
such as Spring Boot, typically start a web server automatically and use
inversion of control to call your code, somewhere deep down in tens of
thousands of lines of framework code. The point of this book is to learn
how to do everything yourself, from scratch.
Interestingly, Ktor labels itself a framework. There aren’t any widely
accepted definitions of libraries vs. frameworks. The one I use in this book
is that frameworks automatically and implicitly do things for you, whereas
libraries do nothing unless you explicitly tell them to. Ktor nicely fits this
definition of a library.
dependencies {
implementation("io.ktor:ktor-server-core:2.1.2")
implementation("io.ktor:ktor-server-
netty:2.1.2")
}
package kotlinbook
import io.ktor.server.application.*
import io.ktor.server.engine.*
import io.ktor.server.netty.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
fun main() {
embeddedServer(Netty, port = 4207) {
routing {
get("/") {
call.respondText("Hello, World!")
}
}
}.start(wait = true)
}
Listing 2-1 Start a web server in the application main function at
src/main/kotlinbook/Main.kt
fun Application.createKtorApplication() {
routing {
get("/") {
call.respondText("Hello, World!")
}
}
}
Listing 2-2 Configuring Ktor in a separate isolated function
Using Lambdas
Lambdas are everywhere in Kotlin. In fact, you’ve already written a few of
them. The functions embeddedServer, routing, and get all take
lambdas as arguments.
Lambdas are blocks of code, just like functions. They are created using
curly brackets, { and }. The main difference between lambdas and regular
functions is that a lambda does not have a name, but a function does.
get("/", { ... })
get("/") { ... }
embeddedServer(Netty, 4207) {
createKtorApplication()
}.start(true)
You can’t name it anything you want. It must be the same as the name
of the argument in the implementation of the function.
Figure 2-1 Your web app is up and running and responding to the port defined in your code
Logging
High-quality logging is a vital component of production-grade web apps. It
can make or break your ability to find out what happened when errors
happen in production.
When you run your application, the console prints the error messages
shown in Figure 2-2.
Figure 2-2 SLF4J issues an error message when your application runs
dependencies {
implementation("ch.qos.logback:logback-
classic:1.4.4")
implementation("org.slf4j:slf4j-api:2.0.3")
When you restart your web app, you’ll no longer see a warning from
SLF4J. Instead, you’ll see actual log output, like what you can see in Figure
2-3.
Figure 2-3 Logback fills your logs with verbose information because you haven’t configured
Logback yet
The log output visible in Figure 2-3 is from Netty, the server
implementation used by Ktor. In other words, what you see is the log output
from a dependency of a dependency, multiple steps away from your own
code. The ability to investigate what all parts of the system are up to is what
makes logging so useful.
The most common way to set up a logging implementation is to use the
info level for all third-party code and the debug level for your own code.
Debug log statements from third-party code are usually not relevant unless
you’re working on changes to that code or you’re debugging an issue with
an unknown cause. For your own code, the debug level log output is truly
relevant, as you are in fact working actively on changing that code.
To configure Logback, you need to add an XML config file to
src/main/resources/logback.xml. Listing 2-3 shows an example of a valid
Logback config file.
<configuration>
<appender
name="STDOUT"
class="ch.qos.logback.core.ConsoleAppender"
>
<encoder>
<pattern>%d{YYYY-MM-dd HH:mm:ss.SSS}
[%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="STDOUT"/>
</root>
The <appender> in Listing 2-3 tells Logback where to write the log
output. You can tell Logback to write to files and even send emails. Here,
you configure Logback to simply write logging output to the console. This
is the most common configuration for production setups as well, as most
production environments will have facilities to make console output
available to developers.
The <root> logger in Listing 2-3 is the default logger that Logback
will use for all code – your own, as well as third-party dependencies – that
does not have a specific logger configuration. It’s set to the info level,
which means Logback will only show statements at info, warn, error, and
fatal.
The statement <logger name="kotlinbook"
level="DEBUG"/> in Listing 2-3 overrides the <root> logger for all
logging statements coming from kotlinbook. This means Logback will
show all log statements for your own code at the debug level and above.
Figure 2-4 shows what the log output looks like, after you’ve added the
Logback XML config file.
Figure 2-4 Logback has much nicer output when you’ve configured it to only show info-level logs
for third-party dependencies
import org.slf4j.LoggerFactory
In the logback.xml configuration file, you specified that you want log
output at the debug level for loggers that start with the name
kotlinbook. You’ve named your own logger kotlinbook.Main, and
your log statement is at the debug level, so when you add the code in
Listing 2-4 to Main.kt and run your web app again, you should see your
logging statement printed in the console, like what you see in Figure 2-5.
Figure 2-5 Your logging configuration and your logging statement are set up so that you can see the
log text in the log output
You should always name your logger the same as the package name
plus the name of the file where you create the logger. The name
kotlinbook.Main follows this rule.
You typically create a single logger at the top of the file where you want
to write to the log, below the import statements and before any function or
class declarations.
In Listing 2-5, you can see a full example of what logging from your
own code might look like in your existing main function.
package kotlinbook
import ...
import org.slf4j.LoggerFactory
val log =
LoggerFactory.getLogger("kotlinbook.Main")
fun main() {
log.debug("Starting application...")
embeddedServer(Netty, port = 8080) {
// ...
Listing 2-5 Create a logger and write to the log from your main function in
src/main/kotlin/kotlinbook/Main.kt
Tip If you for some reason don’t want to access the logs from
dependencies that use SLF4J, you can disable SLF4J entirely and
suppress the error it logs on startup when it can’t find a logging
implementation. Add a dependency to org.slf4j:slf4j-
nop:1.7.36 in build.gradle.kts, and you won’t hear from SLF4J
again.
In my experience, SLF4J and Logback are so stable and mature that you’ll
never have problems with them. Magic can be infuriating when it stops
working, but you’ll be hard-pressed to bork your logging setup when you
set it up as I’ve described it in this chapter. Logging is also one of those
things that most programmers accept as being slightly magic and opaque.
Even programmers in the Clojure community, which is probably the least
magic-prone community on the Java platform, will often use Logback and
XML config files in their projects.
You now have a full-fledged logging setup in your web app, with no
boilerplate other than having to invoke the LoggerFactory in each file
where you want to log, and you’re using all the tools that the rest of the
Java community is using. It’s amazing what you can achieve without
frameworks these days.
dependencies {
implementation("io.ktor:ktor-server-status-
pages:2.1.2")
fun Application.createKtorApplication() {
install(StatusPages) {
// Configure the plugin here
}
routing {
// Existing routing is here...
Listing 2-6 shows a full example of how you can use the exception
handling hook in the StatusPages plugin to tell Ktor exactly what you
want to happen when your code throws an unhandled exception.
install(StatusPages) {
exception<Throwable> { call, cause ->
kotlinbook.log.error("An unknown error
occurred", cause)
call.respondText(
text = "500: $cause",
status = HttpStatusCode.InternalServerError
)
}
}
Listing 2-6 Use the StatusPages plugin to make Ktor show useful info about errors, instead
of a blank page
3. Configuration Files
August Lilleaas1
All web apps need configuration files. Configuration files let you move things like
database credentials, API keys, and which port to run the HTTP server on away from
your source code and into a separate configuration file.
Your application will also run in at least three different environments: local
development, automated testing, and production. Configuration files enable having
different defaults in each environment, all checked into version control and easily
reproducible.
You could get through the examples in this book without configuration files, but
then you wouldn’t learn how to build production-ready web apps from scratch.
If you’re new to Kotlin, here are some language features you’ll see examples of in
this chapter:
Defining classes with constructors
Data classes
let blocks
The it shortcut for single-argument lambdas
Null safety and nullable types
Metaprogramming
Regular expressions
The Elvis operator
Single-expression functions
Also in this chapter, you will learn the following about using configuration files in
your web app:
Making properties configurable instead of hard-coded
Storing configuration in a configuration file that’s checked into version control
Having different default configuration values for different environments
Providing secrets such as production password and API keys through environment
variables, without checking them into version control
Logging your config on application startup, for transparency and ease of debugging
Instead of hard-coding values in your source code, you’ll place them in a
configuration file. You’ll use the Typesafe Config library to manage configuration. In
Appendix B, you’ll learn about how to replace Typesafe Config with Hoplite.
httpPort = 4207
Listing 3-1 The app.conf configuration file, specifying which HTTP port the web app will use
The HOCON file format is very straightforward and readable. Each line consists of
a config property name (httpPort), an equals sign (=), and the value to assign to that
config property (4207).
implementation("com.typesafe:config:1.4.2")
This gives you a Config object, which has everything you need to access the
properties you define in your configuration file. For example, to get the value of the
configured HTTP port, you can call config.getInt("httpPort").
The Config object has several methods available to get config values as a specific
type. getInt, getLong, getString, getBoolean, etc. will all give you the
configured value with the type you specify in the method call.
Tip Use auto-completion to see which getters are available on the Config object.
Type config.get in your code and then wait, and you’ll see a popup that shows
all the available methods on Config that start with get.
Typesafe Config will also perform validation and throw an exception if it’s not able to
convert to the type you want. For example, config.getBoolean("httpPort")
will fail, as Typesafe Config doesn’t know how to convert 4207 into a Boolean.
Listing 3-2 shows a full example of how to load and access configuration files in
your application and how to use the httpPort from app.conf instead of hard-coding
the port number in your Kotlin source code.
This looks just like a normal Kotlin class, except it starts with the keyword data to
indicate that it’s a data class and not a plain class.
The code inside the parentheses are the constructor arguments. By adding val
before the name of the argument, you indicate to Kotlin that in addition to taking the
httpPort as the first constructor argument, you also want to store it as a property on
the class instances. val indicates an immutable property, whereas var makes it
mutable.
To create an instance of this data class, you construct it like you would any other
class in Kotlin:
WebappConfig(4207)
As you saw in Chapter 2, you can use named arguments so that people who read
this code can understand what the number 4207 means. In fact, it’s a convention in
Kotlin to always use named arguments for data classes. This makes the code more
readable, and when you have multiple constructor arguments, you don’t have to pass
them in the same order as they’re defined in the data class:
WebappConfig(
httpPort = 4207
)
WebappConfig(
httpPort = config.getInt("httpPort")
)
Your configuration will now fail early. As you load the config and create the
instance of WebappConfig, you’ll immediately call getInt on config, which will
fail if Typesafe Config is unable to cast it to an integer.
It’s also type-safe on access. Kotlin knows that you named the config property
httpPort. Typing config.getInt("http proot") would cause a runtime
error, but config.httpProot will fail when you compile your code.
You will still get a compile error if you try to set httpPort to null. But you can
set dbPassword to null, such as WebappConfig(httpPort = 4207,
dbPassword = null). Data classes also let you omit nullable properties with
WebappConfig(httpPort = 4207), as a convenient shortcut for setting them
to null.
WebappConfig(
httpPort = 4207,
dbUsername = config.getString("dbUsername")
)
Listing 3-5 Passing the platform type String! to the non-nullable type String
return WebappConfig(
httpPort = rawConfig.getInt("httpPort")
)
}
Listing 3-6 Creating a function for loading configuration in your web app
You now have the same WebappConfig as before, but you’re storing the values
in a config file, instead of hard-coded into your source code.
fun createAppConfig() =
ConfigFactory
.parseResources("app.conf")
.resolve()
.let {
WebappConfig(
httpPort = it.getInt("httpPort")
)
}
Listing 3-7 Create the WebappConfig using a let statement
A let block is not special or magical syntax. It’s just a function, and you can call
let on all types. let takes a single lambda and returns whatever the lambda returns.
In this case, let returns an instance of WebappConfig, as Lambdas will return the
last statement in them. You don’t have to explicitly write return for a lambda to
return something.
The lambda in let also gets a single argument passed to it, which is whatever
object or value you called let on. In this case, you called let on
ConfigFactory.load(), which returns a Config object from Typesafe Config.
So the lambda in let gets this Config object passed to it.
A lambda with a single argument is so common that Kotlin has a shortcut for
referring to that argument: it. You could write .let { myThing ->
doSomething(myThing) }, but to save you some typing and to have a common
name that’s easy to recognize, you can also write .let { doSomething(it) }.
Also note that when a function body is a single statement, you don’t have to specify
the return type of the function, and you can replace the curly brackets and the return
with an equals sign and the single statement that makes up the function body. Kotlin
calls this a single-expression function.
This code uses the Elvis operator, which is a convenient way to specify a default
value when something is null or false. The statement
System.getenv("KOTLINBOOK_ENV") ?: "local" means that if the call to
System.getenv() returns null, the statement will return "local", the value on
the right side of the Elvis operator. This sets your config loading up so that you don’t
have to set the environment variable KOTLINBOOK_ENV for your code to work.
Make sure you create an empty app-local.conf before you run this code. The
default environment is "local", and the code will always try to load the
environment-specific config file. Typesafe Config will fail if the file you specified isn’t
on the class path.
httpPort = 4207
Listing 3-10 app.conf explicitly specifies the value of httpPort
httpPort = ${KOTLINBOOK_HTTP_PORT}
Listing 3-11 app-production.conf sets httpPort to the value of the environment variable
KOTLINBOOK_HTTP_PORT
This solution makes it easy to reason about how you mapped environment variables
to config properties – it’s right there in the config file. It also has the benefit of failing
early if you inadvertently forgot to set the environment variable
KOTLINBOOK_HTTP_PORT in production. Instead of falling back to your default
setting, Typesafe Config will crash with a useful and informative error message if the
environment variable for some reason isn’t available to your web app process, as seen
in Figure 3-1.
Figure 3-1 Typesafe Config throws an error if it can’t find a specified environment variable
Note that while it’s not a technical requirement, it’s common to use uppercase and
underscores for environment variable names. You should also prefix the environment
variables with the name of your system, so that you avoid naming conflicts with other
environment variables. This makes KOTLINBOOK_HTTP_PORT a good name for an
environment variable.
WebappConfig::class.declaredMemberProperties
.sortedBy { it.name }
.map { "${it.name} = ${it.get(config)}" }
.joinToString(separator = "\n")
Listing 3-12 A more readable string representation of your WebappConfig data class
This is a good example of functional programming in Kotlin. You start with a list of
properties, call a function to sort them that returns the sorted list, map over the list to
create a new version of it, and finally join it to create a string representation.
Masking Secrets
You don’t want to log things like database passwords and API keys. Printing secrets to
the log adds an attack vector for hackers to exploit, and you should take security
seriously from the get-go.
Listing 3-12 contains a custom string representation of your WebappConfig. You
can modify it so that properties that are likely to contain secrets will have the value
masked out, instead of printed as is.
Listing 3-13 shows how you can use a regular expression to match config property
names and avoid printing the full config value for config properties that contain secrets.
WebappConfig::class.declaredMemberProperties
.sortedBy { it.name }
.map {
if (secretsRegex.containsMatchIn(it.name)) {
"${it.name} =
${it.get(config).toString().take(2)}*****"
} else {
"${it.name} = ${it.get(config)}"
}
}
.joinToString(separator = "\n")
Listing 3-13 Filter out secrets from the string representation of WebappConfig
As your application grows, you’ll have hundreds or even thousands of individual web
handlers, for different combinations of HTTP verbs and URL paths. The purpose of
these handlers is to run different pieces of business logic. In this chapter, you’ll learn
how to uncouple your business logic and web handlers from Ktor, so that you can use
them in any context.
In later chapters, you’ll run your web handlers in many different environments, not
just Ktor. Uncoupling your web handlers from Ktor is a requirement to make that
possible. For example, you’ll see how to run the same web handlers you use in Ktor
from a serverless environment in Chapter 10.
The implementation of the decoupling itself also serves as a nice opportunity to learn
some important concepts about the Kotlin language.
Be aware, though. I would be careful with implementing this kind of decoupling in a
real-world web app. It introduces complexity and abstraction. It’s unlikely that your real-
world web apps need to support multiple simultaneous libraries and/or deployment
scenarios. So be careful, and exercise good judgment.
If you’re new to Kotlin, here are some language features that you’ll see examples of
in this chapter:
Sealed classes with abstract properties and functions
Function overloading
Functional data-driven programming with maps and lists
Copying data classes
Immutability
Destructuring assignments
Function types
Extension functions
Also in this chapter, you will learn the following about decoupling web handlers
from specific libraries in your web app:
Creating abstractions and connecting them to libraries
Why you should consider not decoupling your web handlers in real-world web apps
In addition to learning how to deploy your web handlers in a serverless environment
in Chapter 12, Appendix A will teach you how to replace Ktor with Jersey.
You’ll expand heavily on this data class throughout this chapter. For example, the
type Any works, and it can be used to represent any (pun intended) HTTP response
body. But it blocks the type system from having any (pun also intended) knowledge of
the contents of the HTTP response. Kotlin has a rich and powerful type system, so you
should avoid the type Any as much as you can.
Instead, you can alter WebResponse to take advantage of the fact that you can
represent the status code and headers the same way for all your HTTP responses. You
can update WebResponse to be a top-level class that doesn’t know anything about the
type or contents of the response body. Then, subclasses of WebResponse can
implement the different body types you want to support.
You need a little bit of boilerplate to make it work. Data classes in Kotlin aren’t
“smart,” so you must specify all the properties you can pass to them – you can’t set them
via class inheritance. You need to set default status code of 200, as well as the default
empty list of headers.
TextWebResponse defines a body property with the type String. When you
create text-based HTTP responses, all you really need to define is that the content is an
arbitrary string of text, and that’s about it.
JsonWebResponse defines a body property with the type Any?. You could
enhance this and create something like JsonMapWebResponse and
JsonListWebResponse, with more specific types. But at the end of the day, most
JSON serialization libraries don’t encode JSON value types in the type system and
encode the input as Object! or Any? anyway, opting to throw errors runtime if you
pass it a value that can’t be JSON encoded. So defining the exact value type of the JSON
body won’t net you any practical benefits.
You create instances of your response classes just like you create instances of any
data class. Listing 4-2 shows some examples of how you can create WebResponse
instances.
TextWebResponse("Hello, World!")
TextWebResponse("Oh noes", statusCode = 400)
JsonWebResponse(mapOf("foo" to "bar"))
JsonWebResponse(listOf(1, 2, 3), headers = mapOf(
"Set-Cookie" to listOf("myCookie=123abc")
))
Listing 4-2 Valid response types of your TextWebResponse and JsonWebResponse classes
You now have the basics in place for representing HTTP responses in your web
handlers, without any coupling to Ktor.
At first glance, this might look like an infinite loop, as the header function does
nothing but call the header function. But the different argument types (single string vs.
list of strings) mean they are two distinct functions and no infinite loop occurs.
The code that does the heavy lifting will reside in header(String,
List<String>). It will need to do two things: All the properties of WebResponse
are immutable, so we must create a new instance of WebResponse with new values.
Additionally, the headers map is also immutable, so we need to create a new
headers value with the new list of headers appended.
To create a new instance of WebResponse, we can lean on the copy function that
is built into all data classes. The WebResponse class itself is not a data class, so it does
not have a built-in copy function. But WebResponse is the class that implements
header(String, List<String>) and therefore needs access to a method that
can create a new copy. You can implement this using an abstract function on
WebResponse, which has no implementation, but returns a new instance of
WebResponse and defers the implementation to the child classes:
The order of the arguments for copy is the same as the order of the arguments in the
constructor of the data class. If you prefer, you can also name the arguments, such as
copy(body = body, statusCode = statusCode, ...). But it saves you
some typing to omit the names. And the implementation of copy lives right next to the
constructor itself in the source code, so it’s easy to locate the actual ordering of
arguments by just looking at the code right there.
All that’s left is to create the actual implementation of header(String,
List<String>) in WebResponse:
You can now use this code to append headers using convenient functions instead of
manually creating maps and lists for the headers:
// Before
TextWebResponse("Hello, World!", headers = mapof(
"X-Whatever" to listOf("someValue")
))
JsonWebResponse(listOf(1, 2, 3), headers = mapOf(
"Set-Cookie" to listOf(
"myCookie=123abc",
"myOtherCookie=456def"
)
))
// After
TextWebResponse("Hello, World!")
.header("X-Whatever", "someValue")
JsonWebResponse(listOf(1, 2, 3))
.header("Set-Cookie", "myCookie=123abc")
.header("Set-Cookie", "myOtherCookie=456def")
Case-Insensitive Headers
To make headers even easier to work with, you can update WebResponse to handle
headers in a case-insensitive way. Specifically, this lets you add "set-cookie" and
"Set-Cookie" and combine them into the same header value, even though one is all
lowercase and the other is Camel-Case.
You can achieve this by leaving the existing code as is and storing the headers in
multiple casings in the headers map. Then you can add a new function that creates a
version of the headers where they’re all lowercased.
The headers map is just data, and Kotlin has plenty of facilities for transforming
data (commonly called functional programming or data-oriented programming), so that’s
how you’ll implement this new function.
The first step is to convert the headers map to a list of pairs and lowercase the keys:
headers
.map { it.key.lowercase() to it.value }
mapOf(
"foo" to listOf("bar"),
"Foo" to listOf("baz")
)
listOf(
"foo" to listOf("bar"),
"foo" to listOf("baz")
)
Notice the subtle difference in casing here. The original map contained the keys
"foo" (lowercased) and "Foo" (capitalized), which Kotlin considers two different
keys. But you will be lowercasing the keys so that they’re both "foo" (lowercased),
and Kotlin maps can’t contain two different values for the same key. But calling the map
function on a map data structure will turn it into a list of Pair. And a list can contain
duplicates. So the final list will contain multiple pairs with the same lowercased key
"foo".
The headers() function in Listing 4-5 returns a new map that has all the header
names in lowercase. Just like the function header(String, List<String>) for
adding new header values in Listing 4-4, the headers() function in Listing 4-5 uses
getOrDefault to merge the values of headers where the names are identical when
lowercased, so that you don’t end up replacing the values in "foo" (lowercased) with
the values of "Foo" (capitalized).
The fold Function
Sit back, take a deep breath, and clear your mind, as I attempt to do the impossible:
explain how functional programming works.
fold is at the core of all list transformations in functional programming. You can
use fold to implement other functional transformations, like map, filter,
forEach, max, take, etc. This makes fold useful in cases where your list
transformation needs some extra rules and isn’t just a plain map or filter.
In the implementation of headers() in WebResponse in Listing 4-5, fold is on
a list and passes an initial value mapOf() (an empty map) and a lambda. fold then
calls the lambda and passes the initial value you provided, res, and the first value in the
list, (k, v). Then, fold calls the lambda again with whatever the lambda returned the
first time fold called the lambda and the second value in the list. Then, this process
repeats for each item in the list, calling the lambda for each subsequent item in the list,
with whatever the lambda returned the last time fold called the lambda, as well as the
list item itself.
The initial empty map is an accumulator. You start with the initial value and
accumulate new values into it each time fold calls the lambda. fold does not care
what type of value the accumulator is. It can be a list, a map, a number, or anything else
you might want to accumulate over.
Destructuring Assignment
(k, v) is a destructuring assignment. Many entities in Kotlin, such as data classes and
Pair, implement componentN() functions. You can access the key of Pair as
it.key, but also as it.component1(). Similarly, you can access the value of a
Pair as it.value, but also as it.component2(). For all entities that implement
componentN() functions, you can destructure them. This is a convenient way to
directly refer to components of an entity, without having to create explicit named
variables for them. Listing 4-6 shows how destructuring and explicit variables are
identical in functionality.
// Using destructuring
val (k, v) = it
Connecting to Ktor
You now have a full stand-alone representation of HTTP responses, which has no
knowledge of Ktor or any other library. However, your web app does use Ktor. So you
need to tie the two together and make Ktor handle your WebResponse classes.
There are two main differences between the two examples in Listing 4-7. First,
there’s no direct interaction with the Ktor call API in the handlers. Second, the Ktor
call API is imperative in that you invoke methods on it imperatively and in succession.
The WebResponse API is functional in nature, meaning that the only point of
interaction is the immutable value WebResponse that’s returned from your own
handlers, with no imperative method invocations.
The first step is to create a function that creates and returns a new lambda. The
function get in Ktor has the (simplified) signature get(path: String, body:
ApplicationCall.() -> Unit). In other words, the second argument is an
extension function on ApplicationCall, with no return value.
Extension Functions
The first step is to create the function webResponse as seen in Listing 4-7. The return
value of webResponse is passed as the second argument to get and needs to match
that type signature. The type signature of get is get(path: String, body:
ApplicationCall.() -> Unit). So webResponse needs to return something
that matches the type ApplicationCall.() -> Unit.
This is a function type with a receiver type. In the original call to get in Listing 4-7,
you pass a lambda as the second argument. Function types are, among other things, how
you set up a Kotlin function to receive lambdas. In other words, lambdas are valid
function types.
The receiver type is ApplicationCall. Lambdas with receiver types are lambdas
where you customize what this refers to inside the lambda.
Extension functions are just like lambdas with receiver types, where you can
customize what this refers to inside the function body, but are full-fledged named
functions instead of anonymous lambdas.
In object-oriented programming, there’s always an implicit this that refers to the
instance you called the method on. Extension functions are a hybrid between instance
methods and plain stand-alone functions. You don’t define extension functions inside
classes or interfaces. Instead, you define them anywhere you want in your code, for any
type that you want.
In Listing 4-8, an example demonstrates an extension function on String. The
function uses this to refer to the string that you called the function on. You invoke the
function as if it were a method defined on the String class itself.
fun String.getRandomLetter() =
this[Random.nextInt(this.length)]
"FooBarBaz".getRandomLetter() // "z"
"FooBarBaz".getRandomLetter() // "B"
Listing 4-8 A demonstration of an extension function on String
Lambdas with receiver types are like extension functions, except they are just
lambdas and not named functions defined on a type. You can only call a lambda with a
receiver type as if it were a method defined on an instance of that receiver type. Listing
4-9 shows an example of implementing and calling a lambda with a receiver type.
fun String.transformRandomLetter(
body: String.() -> String
): String {
val range = Random.nextInt(this.length).let {
it.rangeTo(it)
}
return this.replaceRange(
range,
this.substring(range).body()
)
}
"FooBarBaz".transformRandomLetter {
"***${this.uppercase()}***"
}
// "FooB***A***rBaz
Listing 4-9 A demonstration of a lambda with a receiver type
The argument body represents the lambda. Like extension functions, you call body
directly on an instance of String, as if it’s a method implemented on String itself.
To read more about extension functions, you can read more about them in the official
Kotlin documentation:
https://kotlinlang.org/docs/extensions.xhtml.
fun webResponse(
handler: suspend PipelineContext<Unit, ApplicationCall>.(
) -> WebResponse
): PipelineInterceptor<Unit, ApplicationCall> {
return {
}
}
Listing 4-10 The initial empty skeleton for webResponse
Tip The keyword suspend will be explained in detail in Chapter 8. For now, all
you need to know is that it is a part of the type for Kotlin handlers and that it’s related
to coroutines. Some of the APIs on the ApplicationCall from Ktor require a
suspend keyword to work properly.
webResponse returns a lambda. You don’t need to annotate the lambda itself with
types, as Kotlin has everything it needs to infer the type from the fact you return the
lambda and that you’ve defined a return type for webResponse. Kotlin automatically
turns it into a lambda that matches PipelineInterceptor<Unit,
ApplicationCall>.
Map Headers
The lambda that’s returned by webResponse should contain all the mapping code
between Ktor and the WebResponse class. All instances of WebResponse have a
map of headers, so that’s a good place to start.
You can find an updated version of webResponse in Listing 4-11 that reads the
headers from the WebResponse instance and maps them to Ktor.
fun webResponse(
handler: suspend PipelineContext<Unit, ApplicationCall>.(
) -> WebResponse
): PipelineInterceptor<Unit, ApplicationCall> {
return {
val resp = this.handler()
for ((name, values) in resp.headers())
for (value in values)
call.response.header(name, value)
}
}
Listing 4-11 Mapping the WebResponse headers to Ktor
fun webResponse(
handler: suspend PipelineContext<Unit, ApplicationCall>.(
) -> WebResponse
): PipelineInterceptor<Unit, ApplicationCall> {
return {
val resp = this.handler()
for ((name, values) in resp.headers())
for (value in values)
call.response.header(name, value)
when (resp) {
is TextWebResponse -> {
call.respondText(
text = resp.body,
status = statusCode
)
}
}
}
}
Listing 4-12 Mapping TextWebResponse and JsonWebResponse to Ktor
Map JsonWebResponse
Mapping JsonWebResponse requires a few extra steps compared with mapping
TextWebResponse. You need to add a third-party JSON serializer (something that
takes data and writes JSON strings), and you need to add an implementation of the Ktor
interface OutgoingContent to map a string containing JSON to Ktor.
There are many JSON serializers available. For this book, you’ll use Gson, a Java
library made by Google. Gson is one of the fastest JSON libraries out there
(www.overops.com/blog/the-ultimate-json-library-json-
simple-vs-gson-vs-jackson-vs-json/). It’s also easy to use it in a data-
oriented style, so that you don’t have to create mapping classes and annotations to use it,
which some other JSON libraries on the Java platform require. It works with plan lists
and maps for serializing data and for parsing JSON in later chapters.
Add Gson as a dependency by adding the following to the dependencies block of
build.gradle.kts:
implementation("com.google.code.gson:gson:2.10")
import com.google.gson.Gson
class KtorJsonWebResponse (
val body: Any?,
override val status: HttpStatusCode = HttpStatusCode.OK
) : OutgoingContent.ByteArrayContent() {
This new class KtorJsonWebResponse takes the JSON data in body and an
optional status code in status and maps them all to Ktor by overriding key functions
in OutgoingContent. You can make it more flexible if you want, by enhancing it to
pass in your own ContentType instance that lets you choose other content types and
charsets. But for most cases, it should be enough to be able to hard-code it to the default
application/json and a hard-coded encoding (UTF-8 in this case).
Finally, you need to use an instance of KtorJsonWebResponse in your
implementation of webResponse, where currently you’re only handling
TextWebResponse. Listing 4-14 shows an updated when statement from
webResponse, where you handle both JsonWebResponse and
TextWebResponse.
when (resp) {
is TextWebResponse -> {
call.respondText(
text = resp.body,
status = statusCode
)
}
is JsonWebResponse -> {
call.respond(KtorJsonWebResponse (
body = resp.body,
status = statusCode
))
}
}
Listing 4-14 Handle both TextWebResponse and JsonWebResponse inside the when statement in
webResponse
The meat of the mapping resides inside KtorJsonWebResponse, so all you need
to do is to add a case in the when statement for JsonWebResponse and pass an
instance of KtorJsonWebResponse to call.respond.
get("/", webResponse {
TextWebResponse("Hello, world!")
})
get("/param_test", webResponse {
TextWebResponse(
"The param is: ${call.request.queryParameters["foo"]}"
)
})
get("/json_test", webResponse {
JsonWebResponse(mapOf("foo" to "bar"))
})
get("/json_test_with_header", webResponse {
JsonWebResponse(mapOf("foo" to "bar"))
.header("X-Test-Header", "Just a test!")
})
Listing 4-15 Add routes to Ktor using WebResponse, inside the routing block of your Ktor application
Start your application and try to open all the new paths and see what happens!
A Note on Overengineering
There’s a lot of handling of various edge cases in this stand-alone representation of
HTTP responses. Do you really need all this extra code?
I’ve never actually done something like this in real-world web apps. This is because
I’ve never written a web app that runs both on Ktor and in a serverless environment. I
have done rewrites where I switched from one web framework to another. But you don’t
need to abstract away your web framework up front to make rewrites possible – you can
just do the rewrite, like any other rewrite. I’m not a fan of using an abstraction solely to
ease potential rewrites later in the project.
In the context of this book, the abstraction makes sense, as I want to show you later
how you can easily replace Ktor with Jersey and others and how to run your handlers in
a serverless environment. This is almost trivial to do when you have WebResponse
between your business logic and the web routing library.
Additionally, WebResponse isn’t all that complex. And I personally prefer my web
handlers to be functional in style, which WebResponse is. So I think the trade-off is
worth it. The next time I create a Kotlin web app from scratch, I’ll probably introduce
something like WebResponse to the code base, just because I like the functional style
of it.
But it's all this handling of all kinds of use cases that you may or may not use that
makes frameworks so big and complicated. WebResponse is just a micro example of
this issue. A framework needs to handle hundreds of thousands of cases like this and
provide APIs and interfaces to allow for all of them, in any combination. Frameworks
must therefore contain mountains of abstractions and code that you won’t ever need, and
in turn this is what makes frameworks unapproachable as a unit in your system. If
something goes wrong in the framework you use, you must dig deep into the caves of
Mount Framework that contains hundreds of thousands of lines of code and multiple
layers of abstraction, which can be a daunting task, to say the least.
This, of course, is also the value of frameworks. There’s already a solution available
to you in the framework when you need to do something. And having a standardized
way of doing things makes it easier to work on different code bases, where they all do
things the same way, following framework conventions.
All abstractions have a cost. I think WebResponse is worth it. But be careful.
Don’t abstract away a library just for the sake of abstracting away a library.
Part II
Libraries and Solutions
© The Author(s), under exclusive license to APress Media, LLC, part of Springer Nature 2023
A. Lilleaas, Pro Kotlin Web Apps from Scratch
https://doi.org/10.1007/978-1-4842-9057-6_5
Your web app needs somewhere to store and read data. By far the most common place to do
that is in a SQL database. This chapter goes through all the wiring you need to connect to
your database with a connection pool, how to set up the initial schema, and how to maintain
changes to your database schema over time, as requirements inevitably change.
If you’re new to Kotlin, here are some language features that you’ll see examples of in
this chapter:
Closing and cleaning up resources with use
AutoCloseable and Closeable objects
Avoiding intermediate variables with also
Using functions as lambdas with function references
Also in this chapter, you will learn the following about migrating and querying SQL
databases:
Using Flyway to manage the initial schema and schema changes over time
Using JDBC connection pools to connect to the SQL database
Generating seed data for pre-populating your database with required data
In Chapter 6, you’ll learn more about executing queries against your database.
Tip You can use another database than H2 if you want to. Make sure you also alter the
SQL to match your database. Most of the SQL in this book is straightforward and should
work outside of H2, but some things like CREATE TABLE statements and the data types
are usually different between the SQL databases.
You also need a library for creating the connection pool, and the library you’ll be using is
HikariCP. It’s a generic library that implements the JDBC DataSource interface for
connection pools and is a tried-and-true production-grade library that I’ve used in plenty of
real-world projects, in Clojure, Groovy, and Kotlin.
To install H2 and HikariCP, add them as dependencies to the dependencies block in
build.gradle.kts:
implementation("com.zaxxer:HikariCP:5.0.1")
implementation("com.h2database:h2:2.1.214")
The H2 dependency includes the database driver, as well as the actual implementation of
the H2 database. Normally, the database driver only contains what’s necessary to connect to
an external database. But H2 is an embedded database and therefore includes both driver and
server in one convenient package.
Setting Up H2
When you connect to any database, you give the connection pool a URL that it connects to.
H2 is an embedded database, so the connection URL is also where you configure H2 and
how it operates.
H2 has two main modes of operation: in-memory and with file system persistence. The
in-memory mode means that H2 completely wipes out the database when you restart your
web app. To make H2 use the in-memory mode, you set it up using the URL
jdbc:h2:mem:mydbname.
In this book, you’ll use the file system mode, though. While the in-memory mode can be
convenient, the goal is to learn how to set up real-world web apps using databases like
PostgreSQL and MariaDB, where data is persisted through restarts. The file system mode
makes H2 behave more like those databases. To make H2 use the file system mode, you set it
up using the URL jdbc:h2:path/to/file.
A good location in the file system to place your H2 database is ./build/local. This
places the database file in the build folder, which is the directory that Gradle already uses
to store its build output. If you run the Gradle clean task, it’ll wipe out the entire build
directory, including your database. If you want to make sure Gradle never touches your
database, place it somewhere else. But you shouldn’t be storing important data in your local
development database anyway.
You’ll also set two flags in the connection URL, MODE=PostgreSQL and
DATABASE_TO_LOWER=TRUE. Setting these two flags makes H2 easier to work with, and
it makes H2 not care about whether you spell table and column names in uppercase or
lowercase, among other things.
Updating WebappConfig
You already have a system in place for storing configuration for your web app from Chapter
3, and config files are the perfect place to store database connection credentials.
You also need to set up your configuration file to contain the credentials that the
connection pool will use to connect to your database. H2 doesn’t require a username and a
password. But you’ll set it up with full credentials anyway, to mimic what you would do in a
real-world web app.
First, add the required properties to your configuration files. Your main configuration file
in src/main/resources/app.conf should contain default or empty versions of all the
configuration properties in your system. Listing 5-1 shows the properties you need to add.
httpPort = 4207
dbUser = null
dbPassword = null
dbUrl = null
Listing 5-1 Add default empty values to src/main/resources/app.conf
You also need to specify the values you’ll be using in your local development
environment, which you do in the config file src/main/resources/app-local.conf. H2 accepts
empty strings for the database username and password. All you need to do, shown in Listing
5-2, is to set the database URL so that HikariCP knows that it should use H2.
dbUser = ""
dbPassword = ""
dbUrl =
"jdbc:h2:./build/local;MODE=PostgreSQL;DATABASE_TO_LOWER=TRUE;"
Listing 5-2 Configure your local dev environment to use H2, in src/main/resources/app-local.conf
Finally, you need to update your configuration data class to extract and store the database
connection values. Listing 5-3 shows how to add the required properties to the data class and
extract them from the config file.
This sets you up with a WebappConfig that’s populated with the database connection
credentials required when you want to connect.
Tip Data source and connection pool are both terms that refer to the same thing. The
Java platform names the API for using connection pools javax.sql.DataSource.
Any time you see an API that requires a data source to work, you can pass it your
HikariCP connection pool.
Later in this book, you’ll be using the connection pool in multiple places, so you’ll create a
function for setting up the connection pool. Listing 5-4 shows how to call HikariCP and pass
on the relevant details from your WebappConfig.
Creating a Connection
To test that everything works correctly, create a connection pool in your web app
initialization, and try to run a query. Listing 5-5 shows how to use the JDBC API to perform
a simple test query.
SELECT 1 is a good query to test that everything is up and running. It works for empty
databases without any tables. If executeQuery runs without throwing any exceptions, it
means HikariCP was able to create a connection and that the query executed successfully
inside the H2 database engine.
getConnection() is the API on connection pools to obtain a connection you can use
for executing queries. createStatement() is the function on a connection to create a
JDBC query statement. Finally, executeQuery() is the function to execute arbitrary SQL
queries against your database.
Database connections implement the interface java.lang.AutoCloseable.
AutoCloseable, and its sibling Closeable, represents objects on the Java platform that
hold on to external resources and have a close() method. The object will hold on to the
external resources potentially forever, until you call close(), so it’s important to remember
to call that method on any closeable objects. For a raw database connection, calling
close() disconnects it. For connections provided by a connection pool, calling close()
just means that you won’t be using that connection more from your code and that you’ve
released it back to the pool. Whether or not the connection pool closes the actual database
connection is up to the connection pool implementation.
use() is a scope function on java.lang.AutoCloseable that automatically calls
close() on the object after the lambda finishes executing. You could manually call
close() on the connection, but using use() has the added benefit of wrapping the whole
operation in a try/catch block so that you close the connection even if control flow breaks due
to your code throwing an exception.
Installing Flyway
The library you’ll use for managing database schema migrations is Flyway
(https://flywaydb.org/). Flyway is an industry-standard library, used by JVM
developers everywhere. It’s what I’ve been using for the last ten or so years in real-world
projects. Just like HikariCP, I’ve been using it from Gradle and Clojure as well as from
Kotlin.
There are other libraries available, such as Liquibase (www.liquibase.org/). I’ve
also used Liquibase in real-world projects, and it’s a solid and stable library. The reason I
prefer Flyway is that you can use plain SQL files for migrations without any boilerplate and
XML configuration files. I don’t like DSLs that attempt to wrap the SQL to make it database
agnostic; I prefer to write SQL that’s specific to the database engine my web app is using. I
also like the way Flyway supports versioned and repeatable migrations based on simple file
name conventions, instead of having to set config flags and write XML.
To install Flyway, add it as a dependency to the dependencies block in
build.gradle.kts:
implementation("org.flywaydb:flyway-core:9.5.1")
Call this function as soon as possible in your main() function, preferably right after you
create your WebappConfig. If you already have a call to createDataSource, replace
it with createAndMigrateDataSource.
You use Flyway by passing it the data source, the location of your database migration
files, and the name of the table where Flyway stores its own metadata. Flyway uses the
metadata table to keep track of its own internal state. You’ll see more about that state later in
this chapter.
Tip You can omit the locations and table method calls in Listing 5-6, as they are
set to their default values. I included them in the listing because they are important
properties you need to be aware of to understand how Flyway operates.
The also function is another scope function like let, use, and apply. The goal of
createAndMigrateDataSource() in Listing 5-6 is to create the data source, run the
migrations, and then finally return the newly created data source. This is exactly what also
does. also first runs the lambda and passes the object that you called also on to that
lambda (dataSource in this case). When the lambda has finished executing, also returns
the object you called it on initially. This saves you from creating an intermediate variable and
lets you write the whole operation as a single statement.
The syntax ::migrateDataSource is called a function reference. You could have
written .also { migrateDataSource(it) }, which would wrap the
migrateDataSource function in a lambda. But you can skip that extra step and just pass
in the function directly. Kotlin doesn’t care if you pass a lambda or a function reference to an
argument that’s a function type, and function references and lambdas with the same
arguments and return types are logically equivalent.
If you run your web app, you’ll see log output from Flyway saying, Migrating schema
“PUBLIC” to version “1 - initial”. This means that Flyway successfully ran the SQL file
and registered the migration as successfully completed. If you rerun your web app, Flyway
will now log, Current version of schema “PUBLIC”: 1 and Schema “PUBLIC” is up to date.
No migration necessary, indicating that the migration file you created has already run
successfully and Flyway didn’t write any changes to the database.
If you rerun your app, Flyway will see the new V2 file, detect that it hasn’t executed it
yet, and execute it.
The second way of solving this, which is my preferred way, is to do the change in
multiple steps. What if you don’t want to have a default value? I prefer to have default values
in my business logic, not in my database. If I forget to set tos_accepted from my code, I
want the operation to fail.
You can do this by splitting the operation to three steps: add the column and allow nulls,
run an UPDATE statement that sets the column to the preferred value, and then update the
column with a NOT NULL constraint. Listing 5-10 shows how to write that migration.
This way, your database ends up in the desired state. New inserts will fail if you don’t set
tos_accepted. And existing records get the new tos_accepted column set to false.
Backward-Compatible Migrations
It’s common to run your web app in blue/green deployments, meaning that when you deploy
the latest version of your code, you keep the previous version up and running until the
process with your new code has started up and is fully initialized. Then, your deployment
setup directs all incoming requests to the latest version and only then shuts down the previous
version.
The consequence of this is that for some time, the previous version of your code is
running while the latest version of your code is starting up and running the database
migrations. So the previous version of your code is now running against the latest version of
the database schema.
If you’re not careful, you’ll make changes to your database schema that are incompatible
with the previous version of the code. Therefore, it’s helpful to write your database
migrations to be backward compatible with the previous version of your code.
The migration in Listing 5-10 is not backward compatible. It runs successfully, but it also
makes a change to the database that causes the previous version of the code to fail. If the
previous version of the code tries to insert a new user, the operation will fail as it has no
knowledge of the new tos_accepted column and doesn’t set it.
You can solve this by leaning on the fact that you have blue/green deployments and that
deploying the latest version of your code does not incur any downtime on your system.
Listing 5-11 shows an updated version of the V3 migration, where all you do is to add the
new column.
Deploy this version first and wait until it’s completed. Remember, when you deploy in a
blue/green environment, the previous version of the code runs only temporarily, and the
blue/green deployment shuts the old code down before the deployment is complete. The next
step is to create a new V4 migration, which finishes the job. The previous version of the code
is no longer running in production, and the current code in production knows about the
tos_accepted column. Listing 5-12 shows the new V4 migration, which contains the
remaining changes to mark the new column as NOT NULL.
If you look at the V3 and V4 migrations from Listings 5-11 and 5-12, you can see that
they are identical in content to the original V3 migration from Listing 5-10 that did the entire
operation in one step. So the only change that you made was to run it in two steps, instead of
one, to make sure that your migrations are always compatible with the current and previous
versions of code that are running in your production environment.
Be aware, though. This migration was easy to make backward compatible. But this is not
always the case. The larger your change, the more complex your backward-compatible
migrations can become, compared with writing it in a non-backward-compatible way. You
should consider whether to make a migration backward compatible on a case-by-case basis.
If the cost is too high, risking some downtime or exceptions in productions might be
acceptable.
Repeatable Migrations
A good place for static seed data is repeatable migrations. This is a special type of migration
that Flyway will always run every time you ask Flyway to execute your migrations. Flyway
runs them after all the schema change migrations have finished running, so they will always
operate on the latest version of the schema.
Repeatable migrations are just like normal migrations – they’re a SQL file that runs some
operations on your database. To create a repeatable migration, you put it alongside your
normal migrations in src/main/resources/db/migration. But instead of naming the file
V{number}__{myFileName).sql, you name it R__{myFileName}.sql. Repeatable migrations
do not have a version number, as Flyway doesn’t need one. The migration always runs.
You also need to make sure that the actual SQL script is repeatable. Flyway doesn’t do
anything smart or magic when it runs a repeatable migration. If your repeatable migration is
full of INSERT INTO statements, you’ll get duplicated rows in your database every time
Flyway runs your repeatable migration.
How you’ll handle this varies from database to database. PostgreSQL has upsert
statements, Oracle has merge statements, and so on. Listing 5-13 shows an example of how
to write a merge statement in H2 that will either insert a new row or update an existing row,
making the SQL statement safe to put in a repeatable migration.
H2 will check if a row exists with the provided email. If H2 can’t find it, it will insert a
new row. If H2 finds a match, it will update that row instead of inserting a new one.
Setting Up Querying
Building on the connection pool and schema management capabilities of Chapter
5, you need a few more things to be able to comfortably execute queries. In
Chapter 5, you ran a small query using JDBC directly. But the JDBC API is full
of boilerplate and is not that comfortable to work with. So you need a library to
sit between you and JDBC and make your life better.
implementation("com.github.seratch:kotliquery:1.9.0")
Kotliquery is a well-designed library that has all the features you’ll need to
build a production-grade web app. It’s not a popular library, if you judge it by the
number of GitHub stars. But popularity is a vanity metric. I’ve used Kotliquery
successfully in large production systems.
import kotliquery.Row
There are several interesting things happening in this code. Inside the let
block, you create a range from 1 to the metaData.columnCount of the
query results. You can think of ranges as a list of the numbers in that range, so
1..5 is the equivalent of listOf(1, 2, 3, 4, 5). Then, you map over
that range and use a function reference to get the column name.
map(it::getColumnName) is equivalent to map { colIdx ->
it.getColumnName(colIdx) }. It’s the same as if you called
getColumnName on the metaData object (it) with the column index from
the range passed as the first argument. Then, the list of column names are
mapped over to return a pair of the column name and the value of that column
with row.anyOrNull(it). At this point, all you’re doing is extracting the
raw data from the query; you don’t do any type checking or casting. Finally, you
convert the list of pairs to a map with toMap(). The result is that you’ve
converted the Kotliquery Row object to a plain map of column name to column
value.
Creating a Session
To execute queries with Kotliquery, you need the row mapper you just created
and a Kotliquery session.
You create a session by calling the Kotliquery function sessionOf with
your connection pool. A Kotliquery session is a small wrapper around a raw
database connection. The session has methods that you call to execute queries,
and Listing 6-2 shows how you can use single to fetch a single row.
import kotliquery.sessionOf
dbSess.single(queryOf("SELECT 1 as foo"),
::mapFromRow)
// {foo=1}
dbSess.single(queryOf(
"SELECT * from (VALUES (1, 'a'), (2, 'b')) t1 (x,
y)"
), ::mapFromRow)
// {x=1, y=a}
Listing 6-3 Examples of output from single()
The last query selects from two rows, but you only get the first row back
from single(). No special magic happens here; it just relies on whatever
ordering of the result set that the database engine returns.
dbSess.list(queryOf(
"SELECT * from (VALUES (1, 'a'), (2, 'b')) t1 (x,
y)"
), ::mapFromRow)
// [{x=1, y=a}, {x=2, y=b}]
dbSess.list(queryOf(
"""
SELECT * from (VALUES (1, 'a'), (2, 'b')) t1 (x, y)
WHERE x = 42
"""
), ::mapFromRow)
// []
Listing 6-4 Examples of output from list()
You’ll always get a list of rows in return, even for queries that match a single
row. You’ll also get a list if there are zero matching rows. You never need to
check that the return value of list() is null (as the Kotlin type system also
tells you).
You can also use forEach() when your query loads in vast amounts of
data. forEach() will yield data row by row, whereas list() will accumulate
all the results into a list before it yields the data to your code. Memory usage and
performance will be much better if you use forEach() when you expect your
query to return either a high number of rows or rows that contain columns of
binary blobs or other arbitrarily sized items. Listing 6-5 shows an example of
how to use forEach().
dbSess.forEach(queryOf(
"SELECT * from (VALUES (1, 'a'), (2, 'b')) t1 (x,
y)"
)) { rowObject ->
val row = mapFromRow(rowObject)
println(row)
}
// {x=1, y=a}
// {x=2, y=b}
Listing 6-5 Example of using forEach to avoid loading the entire result set into memory
forEach() calls the lambda once for each row that your query yields. It’s
the raw Kotliquery Row object that’s passed to the lambda. You can use the same
mapFromRow() function that you use for single() and list() to convert
this structure to a plain map.
Inserting Rows
To insert rows, you’ll use the updateAndReturnGeneratedKey()
function on the Kotliquery session. You also need to create the session correctly
by setting the flag returnGeneratedKey to true. Listing 6-6 shows how to
create a session and execute an insert correctly. If you don’t set this up correctly,
you won’t be able to access the ID of the inserted row that the database generated
automatically when it inserted the row.
If you’ve followed the examples in this book one by one so far, the ID you’ll
get when you run this insert is 2. That’s because you already created a user using
a repeatable migration in the previous chapter, as seen in Listing 5-13.
dbSess.update(queryOf(
"UPDATE user_t SET name = ? WHERE id = ?",
"August Lilleaas",
2
))
// 1
dbSess.update(queryOf(
"UPDATE user_t SET name = :name WHERE id = :id",
mapOf(
"name" to "August Lilleaas",
"id" to 2
)))
Listing 6-8 Pass query parameters using named parameters
This lets you refer to the parameters by name, and it also has the added
benefit of naming the inputs, so you don’t have to count the position of the
parameter in the list of parameters to figure out what goes where.
Additional Operations
As you can see, Kotliquery has everything you need to perform queries and
inserts. Compared with working with JDBC directly, the Kotliquery API is much
simpler and makes it easy to convert your queries to data and pass in parameters
to your queries.
You can find detailed documentation and API references to the parameters
and methods available in Kotliquery at the project GitHub page:
https://github.com/seratch/kotliquery.
import kotliquery.Session
fun webResponseDb(
dataSource: DataSource,
handler: suspend PipelineContext<Unit,
ApplicationCall>.(
dbSess: Session
) -> WebResponse
) = webResponse {
sessionOf(
dataSource,
returnGeneratedKey = true
).use { dbSess ->
handler(dbSess)
}
}
Listing 6-9 webResponseDb – wrap webResponse and add database querying capabilities
A Note on Architecture
Many frameworks and architectures contain patterns to separate and isolate
different concerns in your web app. For example, the typical hexagonal
architecture aims to loosely couple the components of your web app and make
them interchangeable.
For web handlers, such as the one in Listing 6-10, I don’t do much separation.
The purpose of a web handler is to interact with the database and implement
business logic around it. I consider the database to be a core part of this
interaction. You can separate it out and hide the database from the business logic,
but that has the trade-off of adding opaqueness and friction to understanding
what’s going on in the system.
If I do separate a web handler into multiple parts, it’s with the functional core,
imperative shell pattern. By analogy, you can think of it as a president deciding
what to do and signing documents (functional core) and an executive branch that
executes the orders (imperative shell). Something like 90% of the code ends up in
the functional data-driven core, and that’s where all the business logic resides.
Then, the imperative shell is “dumb” and executes the commands based on the
data generated by the functional core, without any business logic.
When it makes sense, I use queues to separate operational concerns. If one
part of your system writes to a queue and another part of your system reads from
a queue, they’re architecturally and operationally separate and completely
decoupled.
I’m adamant about separating business logic from user interface rendering
logic. The top level of the user interface code fetches data and massages it to
render a “dumb” GUI that has no business logic, and the user interface code just
renders based on the data it receives.
In general, I try to avoid overarchitecting my code. I prefer “dumb” plain
functions and SQL and data that’s easy to trace and follow along with over
firewalling isolation between all components of my web app.
def handleSignup() {
val userRow = createUser(...)
sendEmailToUser(userRow)
}
Listing 6-11 Passing raw maps around to business logic
I rarely do this in real-world web apps. If you spell a property wrong, you’ll
get a null instead of an actual value. You could cast with as, which will throw
an error if the type is wrong or if the value is null. But then you will have to
litter your business logic with this type of casting. And you’ll get an error deep
down in your system somewhere far away from the code that fetches data from
your database.
def handleSignup() {
val userRow = createUser(...)
sendEmailToUser(
userRow.get("name") as? String,
userRow.get("email") as String
)
}
Listing 6-12 Passing individual properties to business logic
This is something I do more often. This way, the business logic is not
dependent on the database structure. All casting happens close to where you
extract the data from the query, so you don’t have to put type casts all over your
business logic. And the casting is now “fail fast,” meaning that you’ll get the cast
error sooner and closer to the code that executes the database query.
dbSess.single(
queryOf("SELECT * FROM user_t"),
::mapFromRow
)?.let(User::fromRow)
// User(
// id=1,
// createdAt=2022-07-27T16:40:44.410258+02:00,
// updatedAt=2022-07-27T16:40:44.410258+02:00,
// [email protected],
// tosAccepted=true,
// name=August Lilleaas,
// passwordHash=java.nio.HeapByteBuffer[pos=0 lim=6
cap=6]
// )
Listing 6-13 Mapping query results to a data class
def handleSignup() {
val user = createUser(...).let(User::fromRow)
sendEmailToUserA(user)
sendEmailToUserB(user.name, user.email)
}
Listing 6-14 How to combine a query result data class with business logic
The issue with sendEmailToUserA is that anyone who wants to call that
function must construct a full User object with all properties, when all that’s
really required to call sendEmailToUserA is the name and the email. You
could make most of the properties on the User data class nullable, but then it
would no longer represent a valid user, as a user without an ID does not make
sense.
Instead, I prefer to write functions like sendEmailToUserB. That way, the
function does not care about aggregates and just operates on the individual data
points it requires to execute.
Use your judgment here, though. Sometimes, passing on the full data class is
advantageous, and it’s difficult to make a general rule for deciding when to do
what.
Database Transactions
Transactions are a hallmark feature of SQL databases. I’ll assume some prior
knowledge, and I won’t go into the many intricate details of modeling business
logic around transactions. You need to learn how to create, commit, and roll back
transactions, though.
Creating Transactions
Kotliquery sessions are not transactional. When you want to execute your
database operations in a transaction, you must explicitly create a transaction by
hand.
To create a transaction, you call the transaction function on your
Kotliquery session and pass a lambda to it. The lambda executes inside the
transaction. Kotliquery commits the transaction when the lambda has finished
running or rolls it back if your code in the lambda threw an exception. Listing 6-
15 shows how that works.
txSess represents the open transaction, and it has the exact same methods
available as the normal Kotliquery session, such as single, list, update,
forEach, etc. If both the INSERT statements in Listing 6-15 succeed, the
transaction is committed. If, on the other hand, one of them fails, the update
function will throw an exception, which the Kotliquery transaction handling code
catches, and it rolls back the transaction instead of committing it.
ByteBuffer is from java.nio.ByteBuffer. The password hash is a
raw byte array, and on the Java platform, byte arrays don’t implement equality.
So you’ll get a warning from the Kotlin compiler if you try to store ByteArray
values directly on a data class. By wrapping the byte array in a ByteBuffer,
the Kotlin data class can correctly compare two separate byte values to see if
they’re equal.
You can also throw an exception by hand if you want to roll back. Or you can
call txSess.connection.rollback() directly.
fun webResponseTx(
dataSource: DataSource,
handler: suspend PipelineContext<Unit,
ApplicationCall>.(
dbSess: TransactionalSession
) -> WebResponse) = webResponseDb(dataSource) {
dbSess ->
dbSess.transaction { txSess ->
handler(txSess)
}
}
Listing 6-16 webResponseTx, a version of webResponseDb that also creates a transaction
You call this function the same way as webResponseDb you created in Listing
6-9. The only difference between webResponseTx and webResponseDb is
that the type of the session is TransactionalSession and Session,
respectively.
fun createUserAndProfile(
txSess: TransactionalSession,
userName: String,
bio: String
) {
txSess.update(queryOf("INSERT INTO user_t ..."))
txSess.update(queryOf("INSERT INTO profile_t ..."))
}
This might seem like a small detail, but it can really save the day. It’s easy to
make a mistake and end up passing a non-transactional Kotliquery session to a
function that requires transactions to operate correctly. By ensuring that those
functions in your business logic that require transactions take a
TransactionalSession, you’ll get help from the type system and avoid
this type of mistake (pun intended).
Nested Transactions
SQL databases don’t support nested transactions. But you can use save points to
mimic them.
Kotliquery does not have built-in functionality for save points, but it’s easy to
create your own small wrapper. This is also a good opportunity to see how
Kotliquery implements the transaction function, as your function for
creating save points will behave similarly. Listing 6-18 shows how you can
implement a dbSavePoint function that creates a save point and makes it act
like a transaction.
The code inside the dbSavePoint lambda will act as if it runs in its own
nested transaction.
A save point is a named entity. If you call setSavePoint without your
own name for it, JDBC will auto-generate a name. You wrap the execution of the
lambda body in try/catch. If the lambda throws an exception, you roll the save
point back. If the lambda didn’t throw any exceptions, you release the save point.
You’re using the scope function also to achieve this. also will return the
object you called it on, but before it returns, it executes the code in the lambda.
This makes also a convenient way to avoid having to create and name an
intermediate variable for the return value of the body lambda.
You also see an example of how you can use generics in plain Kotlin
functions. The A generic represents the return value of the lambda body. This
means that dbSavePoint will pass on the return value of the body lambda, so
that you can maintain a functional coding style while still wrapping your code in
lambdas.
© The Author(s), under exclusive license to APress Media, LLC, part of Springer Nature 2023
A. Lilleaas, Pro Kotlin Web Apps from Scratch
https://doi.org/10.1007/978-1-4842-9057-6_7
You now have all the pieces of the web app puzzle assembled. But before you implement
business logic on your own, you’ll learn how to write automated tests.
If you’re new to Kotlin, here are some language features that you’ll see examples of in this
chapter:
Implementing test cases and assertions with kotlin.test
Null safety with the unsafe cast operator !!
Inline functions
Also in this chapter, you will learn the following about writing automated tests with jUnit
5:
The basics of Test-Driven Development (TDD)
Writing tests that interact with your database
Isolating individual test cases to avoid inter-test dependencies
In Appendix C, you’ll learn how to use the Kotlin test framework, instead of
kotlin.test.
dependencies {
// ...
testImplementation(kotlin("test"))
}
tasks.test {
useJUnitPlatform()
}
Listing 7-1 Add support for kotlin.test and jUnit 5 in build.gradle.kts
Note that if you followed along with the book and created your Kotlin project using IntelliJ
IDEA, you might already have these properties set, as IntelliJ IDEA generates projects with
pre-configured jUnit 5 support.
package kotlinbook
import kotlin.test.Test
import kotlin.test.assertEquals
class UserTest {
@Test
fun testHelloWorld() {
assertEquals(1, 2)
}
}
Listing 7-2 A test class in src/test/kotlin/kotlinbook/UserTest.kt with one test case that fails on purpose
Your test uses assertEquals with two different values to ensure a failed test. 1 is never
equal to 2, so your test should fail.
Also, remember to put all your tests in src/test/… and not in src/main/….
Figure 7-1 Output from running a single test class with IntelliJ IDEA
Running a single test class like this is particularly useful when your test suite has grown to
hundreds or thousands of tests, but you only want to run the specific one that you’re working
on right now.
Also note that you can run a single test function, by clicking the green arrow next to it, for
complete granular control of which tests you want to run.
It’s also vital to know how to run all your tests in one go. Before you push code to
production, you should run all the tests first, to verify that you didn’t break anything. An
automated build environment will also typically run all your tests every time someone pushes
new code.
To run all your tests in one go, open the Gradle control panel in the right sidebar, and
navigate to kotlinbook ➤ Tasks ➤ verification ➤ test. Double-click to run that task and execute
all the tests in your test suite, as seen in Figure 7-2.
Figure 7-2 Output from running your entire test suite with Gradle
As Figures 7-1 and 7-2 signify, you’ve now verified successfully that your setup lets you
know when a test is failing.
class UserTest {
@Test
fun testHelloWorld() {
assertEquals(1, 1)
}
}
Listing 7-3 Update testHelloWorld in UserTest to make it pass
Run the tests again, and you’ll see IntelliJ IDEA telling you that all your tests passed
successfully, as seen in Figure 7-3.
Figure 7-3 Output from running your test suite with no failed assertions
IntelliJ IDEA makes it easy to see that no tests failed, as it only lists failed tests. So, when
the test output panel only shows “Test Results” and a green check mark, you know that
everything is good and that all of your tests ran successfully, without any assertion errors.
As you already have functions available for creating new instances of your config and
connection pool, there’s nothing special you need to do when you create them for your tests.
You simply invoke them, and you get what you need.
Before you run this code, you need to add a missing config file. When you invoke
createAppConfig with "test", it expects to find the file src/main/resources/app-
test.conf and uses it to load config values. Listing 7-5 shows what this file should look like and
what it needs to contain for your tests to run correctly.
dbUser = ""
dbPassword = ""
dbUrl =
"jdbc:h2:mem:kotlinbook;MODE=PostgreSQL;DATABASE_TO_LOWER=TRUE;"
Listing 7-5 The contents of the file src/main/resources/app-test.conf
Note that the purpose of using the PostgreSQL mode is not to perfectly emulate
PostgreSQL. H2 is not able to do that, and you can’t expect your H2 SQLs to run perfectly in
PostgreSQL. The purpose of setting the PostgreSQL mode is to set a collection of properties in
one fell swoop and make H2 more convenient to work with than it is out of the box in the
vanilla mode.
This config file contains the same as app-local.conf, which is database config that
sets your data source up to use an in-memory H2 database. In a real-world web app, it’s likely
that you’ll run against a real database, so it’s useful to be able to separate the config for your
local development database and the database that your automated tests will use.
With this empty implementation in hand, you can now write a test case. You could write a
test that creates the user and use assertNotNull to check that you get a valid user ID and
not null back. But the type system already verifies that, as the return type is Long, not Long?.
Instead, a more useful test could be to create two different users and check that they get a
distinct ID. Listing 7-7 shows what that test case looks like.
@Test
fun testCreateUser() {
sessionOf(
testDataSource,
returnGeneratedKey = true)
.use { dbSess ->
val userAId = createUser(dbSess,
email = "[email protected]",
name = "August Lilleaas",
passwordText = "1234"
)
assertNotEquals(userAId, userBId)
}
}
Listing 7-7 A test case in src/test/kotlinbook/UserTest.kt that checks if createUser works and returns a distinct
user ID
Try to run this test and notice how it fails. You hard-coded createUser to return -1,
which means that the IDs of both the users you created are -1. And your test expects that
when you create two users, they each get a separate and distinct ID assigned to them.
fun createUser(
dbSession: Session,
email: String,
name: String,
passwordText: String,
tosAccepted: Boolean = false
): Long {
val userId = dbSession.updateAndReturnGeneratedKey(
queryOf(
"""
INSERT INTO user_t
(email, name, tos_accepted, password_hash)
VALUES (:email, :name, :tosAccepted, :passwordHash)
""",
mapOf(
"email" to email,
"name" to name,
"tosAccepted" to tosAccepted,
"passwordHash" to passwordText
.toByteArray(Charsets.UTF_8)
)
)
)
return userId!!
}
Listing 7-8 A working implementation of createUser
Run your tests again, and they should pass. The user ID of both users that you created in
the test was the same before, as createUser was hard-coded to return -1. But with a real
implementation that creates users and returns their distinct ID, the test passes.
The double exclamation mark is an unsafe cast operator. The type of
updateAndReturnGeneratedKey from Kotliquery is Long?. This is because it’s
technically possible to write a SQL statement that doesn’t insert any rows, so Kotliquery has to
encode that possibility into the type system. In this case, though, a plain insert that succeeds
and does not throw any exceptions will always return a Long. So it’s safe to cast it from
Long? to Long using !!.
If it for some extraordinary reason ends up actually being null, Kotlin will throw a
NullPointerException at the point where you used the unsafe cast operator !!. So the
rest of your program is still type-safe, as Kotlin will never return a null from a function with a
non-null return type.
Leaky Tests
To demonstrate the problem with leaky tests, you can write another test that also creates a user,
testCreateAnotherUser():
@Test
fun testCreateAnotherUser() {
sessionOf(
testDataSource,
returnGeneratedKey = true)
.use { dbSess ->
val userId = createUser(dbSess,
email = "[email protected]",
name = "August Lilleaas",
passwordText = "1234"
)
If you try to run this test now, you’ll get an error caused by a conflict between the test you
wrote in Listing 7-7, testCreateUser, and the new test you just added. The tests both
create a user with the email [email protected]. The user_t table has a uniqueness
constraint added for the email column, and thus one of the tests fails with the following error
message:
Your tests run against an in-memory database, but this database stays alive for the full test
run. So all your tests run against the same database, and any data that a test leaves behind in
the database after it runs is visible to subsequent tests.
Also note that currently, you use an in-memory database that resets between every test run.
In a real-world project, you’re likely to connect to an actual database where data is persisted.
In that case, you’ll run into issues even with a single test case, as data from the previous test
run is still in the database and causes conflicts on subsequent test runs.
By placing the call to rollback() inside the finally block, you ensure that no matter
what happens in your test, your database always rolls back any changes that you made to the
database in your test case.
You also need to update your test cases to use the new testTx helper function. Listing 7-
10 shows how you can write your tests and wrap them in testTx so that you don’t get any
leakage between your tests.
@Test
fun testCreateUser() {
testTx { dbSess ->
val userAId = createUser(dbSess,
email = "[email protected]",
name = "August Lilleaas",
passwordText = "1234"
)
@Test
fun testCreateAnotherUser() {
testTx { dbSess ->
val userId = createUser(dbSess,
email = "[email protected]",
name = "August Lilleaas",
passwordText = "1234"
)
Run your tests again, and you won’t get an error. That’s because testTx rolls back the
writes in each test before the other ones run, so even though you create both users with the
same email, there’s no conflict.
I also prefer to explicitly write testTx for every test that needs that functionality, instead
of figuring out some clever way to automatically wrap all test cases in a database transaction.
It adds some boilerplate, but you also gain the ability to easily see what goes on in your tests
and how your tests interact with the database. Additionally, you might encounter cases in real-
world projects where you want to write a test that you don’t wrap in a transaction. For
example, you might want to test how two different transactions might interact with one another
in your business logic. All you must do in those cases is to simply not use testTx and instead
do what’s appropriate for that specific test case.
You can then write a test, seen in Listing 7-12, that inserts two users in the system and
asserts that it contains the two users you created and that the number of users it returns is 2.
@Test
fun testListUsers() {
testTx { dbSess ->
val userAId = createUser(dbSess,
email = "[email protected]",
name = "August Lilleaas",
passwordText = "1234")
You might be surprised to see that this test does not pass. In Chapter 6, you wrote a
repeatable migration that inserts a user into user_t. So the number of users in your system
after you insert two users is 3, not 2.
One way to solve this is to simply remove assertEquals(2, users.size). You
assert that userAId and userBId are both present in the result of listUsers() and you
can argue that that’s enough.
Another way of solving this is to use relative asserts. You could argue that you don’t care
that listUsers() returns exactly two results. Another correct definition of listUsers()
is that the number of users returned should increase by 2 after you create two users. Listing 7-
13 updates the test from Listing 7-12 with a check for the relative difference before and after
you create the users.
@Test
fun testListUsers() {
testTx { dbSess ->
val usersBefore = listUsers(dbSess)
With this change, the test only cares about the changes it made to the system and is not
dependent on the specific state of the system before the test started running.
Test-Driven Development
So far in this chapter, you’ve written tests using Test-Driven Development (TDD). That means
that you’ve first written a test that failed and then you updated your implementation and finally
made the test pass successfully.
// Code
fun getUser(dbSess: Session, id: Long): User? {
return null
}
// Test
@Test
fun testGetUser() {
testTx { dbSess ->
val userId = createUser(dbSess,
email = "[email protected]",
name = "August Lilleaas",
passwordText = "1234",
tosAccepted = true
)
assertNull(getUser(dbSess, -9000))
Make sure you put the code somewhere in src/main/kotlin/kotlinbook and the test in
src/test/kotlin/kotlinbook/UserTest.kt.
This test will fail. The initial assertNull will run successfully. You don’t have a user
with the ID -9000 in your system. In fact, you’ll never have users with negative IDs. So this
test successfully verifies that calling getUser with a nonexisting user returns null. But the
later assertNotNull will fail. Your end goal is for that test to pass successfully, as the user
ID returned by createUser should always correspond with a user fetched by getUser.
But getUser is currently hard-coded to return null.
If you rerun your test, it should run without any assertion errors. You’ve now verified that
if you haven’t implemented getUser correctly, the test will fail. And by writing the test first,
you made sure that it was easy to invoke getUser without any context other than database
state and a Kotliquery session.
Notes on Methodology
You now have everything you need to write and run tests that execute your business logic code
and assert that they yielded the expected and required outputs. For the remainder of this
chapter, you’ll learn more about the methodologies of testing and the thoughts behind why
you’re doing what you’re doing.
In this chapter, you’ll learn how to perform service calls (i.e., call external
APIs) and how to do so efficiently. Few web apps operate in isolation, and
most real-world web apps use a combination of a database and calls to
external services to do their work.
If you’re new to Kotlin, here are some language features that you’ll see
examples of in this chapter:
Using coroutines from different contexts
The difference between kotlin.coroutines and
kotlinx.coroutines
Also in this chapter, you will learn the following about parallelizing
service calls with coroutines:
The difference between blocking and non-blocking code
Handling race conditions
Efficiently calling external services is extra useful if your web app runs in
an environment with a micro-service architecture. In those cases, you quickly
end up in a situation where you do hundreds of parallel calls, using outputs
from some calls as inputs to other calls and so on.
get("/ping", webResponse {
TextWebResponse("pong")
})
post("/reverse", webResponse {
TextWebResponse(call.receiveText().reversed())
})
}
}.start(wait = false)
Listing 8-1 A fake service with some route handlers that you’ll use to demonstrate coroutines
Understanding Coroutines
Before you start implementing parallelized service calls to coroutines, you
should know a little bit about what a coroutine does and what makes them
tick.
The delay Coroutine
You’ve already written your first coroutine. Did you notice?
In Listing 8-1, you wrote a web handler that generates a random number,
and before it returns that number as text, it calls a function delay() with
that random number. delay() is a coroutine!
delay() is a special function that only works inside a coroutine context.
If you try to call delay() at the top level or just inside your main()
function, you’ll get an error, shown in Listing 8-2.
Why is it that you’re allowed to write delay() inside your web handlers,
but not everywhere in your code? And why are you using delay() in the
first place, instead of more familiar functions such as Thread.sleep()?
Coroutine Contexts
You can only call suspend functions from two places: coroutine contexts or
functions tagged with suspend.
The error message you got from Listing 8-2 indicates this requirement.
Kotlin can’t just suspend execution at arbitrary points in your code, as the Java
platform does not allow for that. Listing 8-3 shows how you can create a
suspend function and call delay() inside it.
fun main() {
log.debug("Starting application...")
myCoroutineFunction()
// Error: Suspend function 'myCoroutineFunction'
// should be called only from a coroutine or
another
// suspend function
Listing 8-4 Invoking a suspend function from inside main and getting an error message
One way to solve this is to tag the main() function itself with suspend.
The Kotlin compiler will do what’s necessary under the hood and bridge a
plain Java platform main function with a separate coroutine-scoped main
function that Kotlin will call under the hood.
Another way is to explicitly create a coroutine context yourself. Kotlin has
the function runBlocking to bridge the gap between normal execution and
coroutines. Listing 8-5 shows an example of how you can use it to invoke
suspend functions from a non-coroutine context.
fun main() {
log.debug("Starting application...")
Coroutines in Ktor
With all that explanation out of the way, you can now return to the delay()
in your “fake” external service that you wrote in Listing 8-1.
The reason you can write delay() inside your web handlers is that Ktor
handlers already run in a coroutine context. Ktor was written from scratch
with Kotlin in mind and uses coroutines extensively under the hood. Since
most of the code in Ktor runs in a coroutine context already, it exposes this
coroutine context to your web handlers so that you can start using coroutines
in Ktor without any extra setup.
Under the hood, Ktor supports many different server implementations. In
this book, you’ve used Netty, which is built on top of the non-blocking NIO
(new I/O) API added to the Java platform way back in Java 1.4. The mapping
between Netty and Ktor leans heavily on the non-blocking nature of Netty and
makes for an efficient pairing where almost no thread blocking needs to
happen.
Additionally, when you implemented webResponse in Chapter 4, you
included the suspend keyword on the handler lambda where you build
and return your WebResponse instances. The Ktor type
PipelineContext<Unit, ApplicationCall> that is the receiver
type of your handler lambda implements the built-in interface
coroutineScope, which is what tells Kotlin that your web response
handler lambdas have a coroutine scope available to them. And since the
handler lambda function type includes suspend, you have everything you
need to invoke coroutines from inside your webHandler wrapped Ktor
handlers.
Adding Dependencies
You need two dependencies before you can perform external service calls: you
need a HTTP client to invoke the “fake” server and you need a dependency to
the Kotlin coroutine library itself.
There are plenty of HTTP clients available for the Java platform. For this
book, you’ll use Ktor’s own HTTP client. The Ktor HTTP client has no direct
coupling to the Ktor HTTP server. The reason you’re using it is that it’s
fundamentally based on coroutines. So it’s a good fit for Ktor web apps where
you use coroutines.
Listing 8-6 shows what you need to add to make everything work.
implementation("io.ktor:ktor-client-core:2.1.2")
implementation("io.ktor:ktor-client-cio:2.1.2")
Listing 8-6 Adding the coroutine library as a dependency in build.gradle.kts
Like the Ktor server, the Ktor client supports multiple different back ends.
The CIO (coroutine I/O) back end is the coroutine-based one that you’ll use in
this book.
dbSess.single(
queryOf(
"SELECT count(*) c from user_t WHERE email !=
?",
pingPong
),
::mapFromRow)
}
TextWebResponse("""
Random number: ${randomNumberRequest.await()}
Reversed: ${reverseRequest.await()}
Query: ${queryOperation.await()}
""")
}
Listing 8-7 A function that performs multiple parallel calls to external services
At the core, this is just like any other web handler function. You pass in
some parameters, namely, a Kotliquery database session, and you return a
WebResponse. The main difference comes from the fact that it’s a
suspend function and that it’s wrapped in coroutineScope.
async() is the main workhorse when you want to perform multiple
parallelized service calls. The code you wrap in async() runs immediately,
but asynchronously. When you perform the request to /random_number,
your code does not halt and wait for a return value. Instead, it continues to the
next statement, which starts a request to /reverse. Similarly, the code does
not wait for that request to finish, and it moves directly on to the next
statement, which starts yet another async block that performs a request to
/ping and uses that return value as input to a database query.
The asynchronous magic happens when you call await(). The return
value of async() is Deferred<T>, and calling await() on a
Deferred<T> causes that code to suspend (not block). When the result is
ready, your code will continue executing, and the value of your
Deferred<T> will just be the T – in other words, the raw value that the
async() block returned.
async() also requires a coroutine scope to function. Wrapping your
function body in coroutineScope ensures that a coroutine scope is
available. When you call handleCoroutineTest() from a Ktor web
handler, it will inherit the already existing context. So there is no extra
overhead created by wrapping your function in coroutineScope like this.
Adding to Ktor
To test this function, you can create a new Ktor route handler and invoke the
handleCoroutineTest() function from Listing 8-7.
To do this, all you need to do is to add a new route handler in your Ktor
routes block and, as seen in Listing 8-8, invoke the function there.
get("/coroutine_test", webResponseDb(dataSource) {
dbSess ->
handleCoroutineTest(dbSess)
})
Listing 8-8 Invoking handleCoroutineTest() in a web handler
Go ahead and test it now! The actual numbers you get are random by
design, so this is the randomized output I got when I ran the code as I’m
writing this book:
dbSess.single(
queryOf(
"SELECT count(*) c from user_t WHERE email !=
?",
pingPong
),
::mapFromRow)
}
To fix this issue, you should wrap all blocking calls using withContext
and make sure that the blocking calls run on the built-in Dispatchers.IO
context. Listing 8-9 demonstrates how to make the code in Listing 8-7
properly handle blocking calls in coroutines.
withContext(Dispatchers.IO) {
dbSess.single(
queryOf(
"SELECT count(*) c from user_t WHERE email
!= ?",
pingPong
),
::mapFromRow)
}
}
Listing 8-9 Appropriately handle blocking calls in coroutines
With this minor change, you allow the coroutine runtime to execute the
blocking call in a separate thread pool, designated for long-running blocking
I/O operations such as database queries.
In your main web handlers, wrapping your database access calls is not that
important. The typical web handler does one or a handful of sequential
database queries and doesn’t really do any parallelization. But for heavily
parallelized coroutine code, you should keep this in mind and try to remember
to wrap all blocking calls in withContext(Dispatchers.IO).
Coroutine Internals
In this chapter, you’ve already learned some details about what goes on under
the hood of coroutines. In this section, I’ll explain some even deeper details of
how coroutines work. Let’s go under the hood of the hood!
implementation("io.arrow-kt:arrow-fx-
coroutines:1.1.2")
implementation("io.arrow-kt:arrow-fx-stm:1.1.2")
return email.right()
}
if (password == "1234") {
return ValidationError("Insecure
password").left()
}
return password.right()
}
MyUser(
email = validEmail,
password = validPassword
)
}
Listing 8-10 Using Arrow to write functional error handling
The magic happens in the either block in signUpUser.
validateEmail and validatePassword both have the return type
Either<ValidationError, MyUser>, and left() and right()
are extension functions that resolve to either (pun intended) the left or the
right side of the Either. When you call bind() on the Either instance,
you enable the short-circuiting. If the Either returns a left value, Arrow
short-circuits using a continuation under the hood and stops execution of the
either block. It also returns that left value where execution was short-
circuited. On the other hand (pun also intended), if it returns a right value, that
value is bound to the statement, and execution continues to the next line.
To extract the actual value from an Either, you can use fold, which calls
separate lambdas depending on the result. Listing 8-11 demonstrates the
output of a few different calls to signUpUser().
signUpUser("[email protected]", "test")
.fold({ err -> println(err)}, { user ->
println(user)})
// MyUser([email protected], password=test)
signUpUser("foo", "test")
.fold({ err -> println(err)}, { user ->
println(user)})
// ValidationError(error=Invalid e-mail)
signUpUser("[email protected]", "1234")
.fold({ err -> println(err)}, { user ->
println(user)})
// ValidationError(error=Insecure password)
Listing 8-11 Using fold to extract values from Either
import kotlin.coroutines.intrinsics.*
cont.resume(Unit)
// a
cont.resume(Unit)
// b
// c
cont.resume(Unit)
// d
cont.resume(Unit)
// d
Listing 8-12 Using low-level continuations to control execution of code
Notice how the first call to resume the continuation prints “a”, but nothing
else. That’s because haltHere uses the low-level continuation APIs to
indicate to Kotlin that it should suspend execution at this point. After you’ve
executed this first step, it’s completely optional to continue execution of the
coroutine.
When you invoke cont.resume(Unit) for the second time, the
second part of the code runs, and it prints “b” and “c”. Then, simply by the
way continuations function in Kotlin, any further invocations of the coroutine
repeat the last step and cause it to print “d” again and again.
At the end of the day, there is no magic going on. All the perceived magic
comes from the suspend keyword. Kotlin compiles the body of suspend
functions into a state machine that contains the code and a return with a state
machine flag at any point where suspension can occur (i.e., a call to another
suspend function).
Int label;
System.out.println("b");
System.out.println("c");
var10000 = (Continuation)this;
this.label = 2;
if (ContinuationTestKt.haltHere(var10000) ==
var2) {
return var2;
}
}
System.out.println("d");
return Unit.INSTANCE;
}
Listing 8-13 A full example of the compiled output of the createContinuation function
This code is full of things you don’t normally see in Java code, such as
label blocks and breaks with labels (the label17 stuff). But it serves as a
good demonstration to understand how Kotlin manages to not block threads. It
takes your linear Kotlin code in createContinuation and compiles it
into a hard-coded state machine. The initial case 0 first prints "a", steps the
label state from 0 to 1, and then returns a continuation object, var2. The
next time Kotlin invokes the continuation, the case 1 runs, which does
nothing, and continues to the code below the case statement where you print
"b" and "c", and the label state is set to 2. On the next invocation, the
case 2 runs, which just breaks the entire label17 block, and continues to the
code below it, which prints "d". Kotlin doesn’t change the state machine
when this happens, which is why you repeatedly get "d" printed when you
invoke the continuation multiple times after it’s “finished”, as seen at the end
of Listing 8-12.
This code isn’t exactly idiomatic Java code, but that doesn’t matter.
Idiomatic Java is for humans, not computers. You’ll be reading and writing the
Kotlin code, and this compiled output is only there to make it possible for
Kotlin to automatically turn your synchronous-looking code to asynchronous
code that the runtime can interrupt and resume (or continue) at pre-compiled
points in the code.
I find that it really helps to understand the deepest levels of what’s really
going on in coroutines. Turning synchronous statements into a state machine
that Kotlin can interrupt and resume is what enables high-performance
asynchronous kotlinx.coroutines, as well as Arrow’s ability to halt
execution of code after certain states occur, without having to resort to
exceptions.
By compiling your linear-looking Kotlin code to a state machine, you get
to write nice-looking code, without having to block the thread and without
having to resort to writing code like the one in Listing 8-13 yourself.
© The Author(s), under exclusive license to APress Media, LLC, part of Springer Nature 2023
A. Lilleaas, Pro Kotlin Web Apps from Scratch
https://doi.org/10.1007/978-1-4842-9057-6_9
In this chapter, you’ll learn how to generate HTML on the server and how
to set up your web app to serve CSS, images, and other static content.
You’ll also set up authentication and a login form and enhance your
existing user_t table to support production-grade password security and
encryption.
If you’re new to Kotlin, here are some language features that you’ll see
examples of in this chapter:
The safe call operator
Operator overloading
Using domain-specific languages (DSLs)
Details about the precedence rules of extension functions
Object expressions
Also in this chapter, you will learn the following about building
traditional web apps with HTML and CSS:
Using the kotlinx.xhtml
(https://github.com/Kotlin/kotlinx.xhtml) DSL to
build HTML
Making reusable layouts
Logging in and authenticating users
Encrypting and signing session cookies
Hashing and securely storing user passwords
When you’re done, you’ll have a fully working HTML-based setup with
all the necessary components for building a production-grade web app.
Generating HTML
In this section, you’ll learn the essentials for generating HTML and how to
do so from Kotlin and Ktor.
implementation("io.ktor:ktor-server-html-
builder:2.1.2")
The Ktor HTML DSL includes extension functions on the call inside
Ktor route handlers for responding with HTML content built using
kotlinx.xhtml.
The most basic one is call.respondHtml. Listing 9-1 demonstrates
how to use this function, which allows simple inlining of HTML content
inside your web handlers.
get("/html_test") {
call.respondHtml {
head {
title("Hello, World!")
}
body {
h1 { +"Hello, World!" }
}
}
}
Listing 9-1 Using call.respondHtml in Ktor web handlers
That’s all it took to get basic HTML output added to your web app!
Later in this chapter, you’ll learn how to use shared HTML layouts across
multiple web handlers.
Figure 9-2 IntelliJ IDEA shows gray boxes that indicate the receiver type inside lambdas
The lambdas you pass to generateHtml, head, and body have the
respective receiver of HTML, HEAD, and BODY. The DSL then defines the
various functions, such as head and h1, as extension functions of the
respective tag types. So the DSL internally defines h1 as fun
BODY.h1(...) { ... }. (In fact, it’s defined as an extension function
on the more abstract type FlowOrHeadingContent, which BODY is a
child type of.)
Operator Overloading
kotlinx.xhtml leverages operator overloading in its DSL. You’ve
already used one of the overloaded operators: using + to add text content to
HTML tags.
In Kotlin, operator overloading allows custom definitions of otherwise
built-in operators, such as the + sign. You won’t be able to override what +
means on things like numbers, but you can override what + means when
called on your own types.
When you write your kotlinx.xhtml templates, you can use text
instead of +. So h1 { text("Hello, World!") } is the same as
h1 { +"Hello, World" }. The implementation of text is where
the actual code to add text lives. Then, kotlinx.xhtml defines a special
operator function called unaryPlus inside the Tag interface:
package kotlinx.xhtml
interface Tag {
operator fun String.unaryPlus(): Unit {
text(this)
}
}
fun FlowOrHeadingContent.myTag(
block: MY_TAG.() -> Unit = {}
) {
MY_TAG(consumer).visit(block)
}
Listing 9-2 Adding custom tags to the kotlinx.xhtml DSL
fun Application.createKtorApplication(
dataSource: DataSource
) {
// ...
routing {
static("/") {
resources("public")
}
Listing 9-3 Adding a static route to createKtorApplication for serving files inside
Here, the static route is set up to handle the path /. That means all the
files in the public folder on the Java runtime class path are available via
your embedded Ktor web server. For example, if you have a file
public/app.css on the class path, you can access it using
http://localhost:4207/app.css.
The best place to put your assets in the file system is in
src/main/resources. Your Gradle setup already adds all the files in that
folder to the Java runtime class path. You can add as many subfolders in
assets as you like. All the files in src/main/resources/public are recursively
available to the corresponding path in your web app.
Try to create a CSS file now and include it in your HTML output in
your web handler! Listing 9-4 shows how to update the HTML output from
Listing 9-1 to include a stylesheet using the kotlinx.xhtml DSL.
get("/html_test") {
call.respondHtml {
head {
title("Hello, World!")
styleLink("/app.css")
}
body {
h1 { +"Hello, World!" }
}
}
}
Listing 9-4 Linking to a CSS file in the kotlinx.xhtml DSL
The route is unchanged, except from the added styleLink call inside
the head block.
This example demonstrates one of the benefits of using a DSL instead
of plain HTML. The DSL adds convenient shortcuts, so you don’t have to
remember how to fully type out a valid <link> tag for stylesheets. (Is it
href="/app.css" or src="/app.css"? Do you have to remember
to set rel="stylesheet"? And so on.) Note that there is also a link
DSL method available, if you need full control of the attributes of your CSS
links or need to use the <link> tag for other purposes such as linking to
an RSS feed or setting alternate rel="alternate" links with
hrefLang for multilingual sites.
Restart your web app, and refresh your browser, and you should see
your website like in Figure 9-3, with the CSS in app.css applied. In this
case, the CSS file contains body { font-family: sans-serif;
font-size: 50px; } to change the default browser font with a
different one.
Figure 9-3 HTML output with CSS applied
Instant Reloading
One issue with this setup is that you must restart and recompile your web
app every time you make changes to your CSS file.
That’s because the Java runtime loads the files from the class path. And
it’s not actually the folder src/main/resources that’s on the class path when
your web app is running locally. As a part of the compile step, Gradle
moves everything that’s on the class path into the build/ folder, where all
the compiled output lives. Gradle doesn’t compile the files in
src/main/resources, but it does copy them to build/resources/main. It’s those
files that your web app is loading.
This is fine in a production environment and is in fact the preferred
approach. But it’s not really suited for a development environment.
To fix this, you’ll do two things: add a config property for toggling class
path loading vs. file system loading and set up Ktor so it loads from
src/main/resources when configured to do so.
You’ve added config properties before, so I’ll only summarize it here:
In app.conf, add useFileSystemAssets = false. You want this to
be off by default, so you don’t accidentally break your production setup
later. In app-local.conf, enable file system loading by setting
useFileSystemAssets = true. Then, update the WebappConfig
data class with a new property, val useFileSystemAssets:
Boolean. Finally, update createAppConfig to set
useFileSystemAssets to the value of
it.getBoolean("useFileSystemAssets").
Next, you’ll need to update the static route you added to Ktor in
Listing 9-3. You also need to update createKtorApplication to
receive your app config. Listing 9-5 shows how to extend it to load files
from the file system when configured and otherwise load them from the
class path like it does now.
fun Application.createKtorApplication(
appConfig: WebappConfig,
dataSource: DataSource
) {
// ...
routing {
static("/") {
if (appConfig.useFileSystemAssets) {
files("src/main/resources/public")
} else {
resources("public")
}
}
Listing 9-5 Receive app config in createKtorApplication, and load assets directly from
the file system when running your web app locally
This way, Ktor will load your static files directly from your source code
when you run your web app locally, instead of loading copies from the
build/ folder.
You could have named this config property something generic, like
isDevMode, and used the concept of “dev mode” to determine what to do.
But in my experience, more specific config properties are better than
general and reusable ones. If you use the same config property for multiple
different things, you lose the ability to have granular control of your web
app in different environments. Additionally, you’d have to grep your entire
code base for isDevMode to figure out what it does, whereas
useFileSystemAssets is more descriptive.
Reusable Layouts
In most web apps, you’re going to need a layout that’s shared across
multiple routes. A layout adds shared defaults, such as which stylesheet to
load on all pages, which meta tags to set, a header and a footer, and so on.
Instead of copy-pasting the same HTML DSL code, you can create a
reusable layout that you can programmatically reuse in multiple web
handlers.
You’ll also set it up so that it’s completely optional to use the layout in
case you have routes in your web app where you need full control of the
output, without forcing a layout on it.
Adding a Layout
The Ktor HTML DSL has built-in support for layouts. The basic structure
of a layout is to create a subclass of Template<HTML> and implement
the required methods. Listing 9-6 shows an example of a basic, but still
useful, template.
class AppLayout(
val pageTitle: String? = null
): Template<HTML> {
val pageBody = Placeholder<BODY>()
head {
title {
+"${pageTitlePrefix}KotlinBook"
}
styleLink("/app.css")
}
body {
insert(pageBody)
}
}
}
Listing 9-6 A basic Ktor HTML DSL template
The template is just a normal Kotlin class. The class has a single
constructor argument, pageTitle. It’s just a normal class, so you can set
it up to take as many constructor arguments as you need. You use
pageTitle just like a normal property of a normal class, to insert a page
title inside the head block. Plain Kotlin code sets it up to always contain
the name of the app, "KotlinBook" and, if the pageTitle is set,
prefix it so you get "My page title - KotlinBook". This shows
another benefit of using the kotlinx.xhtml DSL. You don’t need a
special templating language to add logic to your templates; it’s all expressed
as plain Kotlin code.
The class also sets up a pageBody property of the type
Placeholder<BODY>. You can have as many of these placeholders as
you like, as they too are plain properties of your template class, and you use
them to insert kotlinx.xhtml DSL code that you provide to the
template when you invoke it. Since it’s of the type BODY, you’re allowed to
call insert(pageBody) inside the body block, as the types all match
up. And when you use the template, the type system knows that the
placeholder can only use tags that are allowed by kotlinx.xhtml inside
BODY.
call.respondHtmlTemplate(AppLayout("Hello,
world!")) {
pageBody {
h1 {
+"Hello, World!"
}
}
}
Listing 9-7 Loading a reusable template in a Ktor web handler
The implicit this inside the lambda is the instance of your template.
That’s why you’re allowed to call pageBody, as it’s just a shortcut for
this.pageBody. All the kotlinx.xhtml DSL code inside the pageBody
block is added to the corresponding location of your AppLayout template,
just as you set it up in Listing 9-6.
Implementing HtmlWebResponse
webResponse and its namesakes are set up to handle any subclass of the
WebResponse abstract class. To be able to handle kotlinx.xhtml,
you need to add a new subclass of WebResponse that holds the desired
HTML output and add a mapping inside webResponse to write that
HTML output to your Ktor web handlers (and, later in the book, other web
frameworks and server environments). Listing 9-8 shows the first step,
which is to implement this new data class.
when (resp) {
is TextWebResponse -> {
...
}
is JsonWebResponse -> {
...
}
is HtmlWebResponse -> {
call.respondHtml(statusCode) {
with(resp.body) { apply() }
}
}
}
Listing 9-9 Implementing and mapping HtmlWebResponse to Ktor in webResponse
class MyThing {
fun String.testMe() {
println("MyThing testMe")
}
}
fun String.testMe() {
println("Top level testMe")
}
"Hello!".testMe()
// Top level testMe
with(MyThing()) { "Hello!".testMe() }
// MyThing testMe
Listing 9-10 Demonstrating extension function precedence
If you want to read about this in full detail, the Kotlin specification
includes complete definitions of all the behavior of the Kotlin compiler:
https://kotlinlang.org/spec/kotlin-spec.xhtml.
get("/html_webresponse_test", webResponse {
HtmlWebResponse(AppLayout("Hello, world!").apply
{
pageBody {
h1 {
+"Hello, readers!"
}
}
})
})
Listing 9-11 A HtmlWebResponse with a layout
get("/html_webresponse_test", webResponse {
HtmlWebResponse(object : Template<HTML> {
override fun HTML.apply() {
head {
title { + "Plain HTML here! "}
}
body {
h1 { +"Very plan header" }
}
}
})
})
Listing 9-12 A HtmlWebResponse without a layout
This creates an inline template that’s only used once, which practically
becomes the equivalent of using no template for that web handler.
A Note on Abstractions
A small but important detail in this setup, which you might not have
noticed, is how unintrusive the webResponse abstraction is. Earlier in
this chapter, you wrote plain Ktor web handlers, using the Ktor call API,
without any interference from the webResponse helper functions and
data classes. By not being too smart about it, and by having to explicitly
“opt in” to the webResponse API by calling methods on it, you don’t
have to find or invent escape hatches to avoid using your own abstraction in
certain cases – you just don’t use them, and that’s all there is to it.
User Security
To enable users to log in, you’ll build a system for authenticating your users
with a username and a password and replace the current hard-coded system
from Chapter 5, where the password is stored as plain text.
Password Hashing
Currently, you have a table user_t with a password_hash column.
You also have a function createUser that writes to the user_t table
and stores the provided password as plain text (by converting it to UTF-8
encoded bytes).
Storing passwords as plain text is a big no-no, and you only did that
earlier for brevity and because you didn’t use the password for anything.
You should always one-way hash your passwords. You shouldn’t even
encrypt the passwords. Encrypting means you also can decrypt, and you
should never need to decrypt passwords.
How do you log in users if you can’t decrypt their passwords? That’s
solved with password hashing. Hashing is a one-way algorithm that will
always produce the same output given the same input, but the output has no
correlation with the provided input and can’t be used to determine what the
original input was. SHA256 and MD5 are examples of hashing algorithms.
The procedure is as follows:
When you create a user, one-way hash the password, and store that hash.
When you want to authenticate a user, one-way hash the password input,
and compare that with the hash in the database.
That way, you have absolutely no way of knowing what user passwords
are, but you have everything you need when you’ll verify password inputs
later.
implementation("at.favre.lib:bcrypt:0.9.0")
The first step is to write a failing test for storing and verifying user
passwords. Listing 9-13 shows an example of what that test might look like.
@Test
fun testVerifyUserPassword() = testTx { dbSess ->
val userId = createUser(
dbSess,
email = "[email protected]",
name = "August Lilleaas",
passwordText = "1234",
tosAccepted = true
)
assertEquals(
userId,
authenticateUser(dbSess, "[email protected]", "1234")
)
assertEquals(
null,
authenticateUser(dbSess, "[email protected]",
"incorrect")
)
assertEquals(
null,
authenticateUser(dbSess, "[email protected]",
"1234")
)
}
@Test
fun testUserPasswordSalting() = testTx { dbSess ->
val userAId = createUser(
dbSess,
email = "[email protected]",
name = "A",
passwordText = "1234",
tosAccepted = true
)
assertFalse(Arrays.equals(userAHash, userBHash))
}
Listing 9-13 Automated tests in UserTests.kt for storing and verifying passwords
fun authenticateUser(
dbSession: Session,
email: String,
passwordText: String
): Long? {
return dbSession.single(
queryOf("SELECT * FROM user_t WHERE email = ?",
email),
::mapFromRow
)?.let {
val pwHash = it["password_hash"] as ByteArray
if (bcryptVerifier.verify(
passwordText.toByteArray(Charsets.UTF_8),
pwHash
).verified)
{
return it["id"] as Long
} else {
return null
}
}
}
Listing 9-14 Implementation of authenticateUser
This function fetches the user row based on the provided email. If no
such user exists in the database, the optionally chained let extension
function causes the whole function to return null. Then, you use the
bcrypt library to verify that the hash stored in the database is the same as
the one provided in passwordText. If it is, you get the ID of the
matching user. If not, you get null.
You also need to update createUser so it hashes the password with
bcrypt before storing it in the database. Replace the current
passwordText.toByteArray(Charsets.UTF_8) with the code
in Listing 9-15.
bcryptHasher.hash(
10,
passwordText.toByteArray(Charsets.UTF_8)
)
Listing 9-15 The code to hash a password string with bcrypt
The first argument, the number 10, is the difficulty factor in bcrypt. This
number decides how much computing power you’ll need to perform the
hashing. Too low, and the password is easy to crack. Too high, and your
servers will spend multiple seconds verifying the password, at 100% CPU
capacity. I’ve used 10 for many years, and that’s worked fine for me. But
ask your local cryptography and security expert before you commit to a
number, and don’t take my word for it.
Run your tests, and everything should pass. You now have real
passwords for your users that are stored securely!
hex(
bcryptHasher.hash(
12,
"1234".toByteArray(Charsets.UTF_8)
)
)
Listing 9-16 A utility function for generating hex-encoded bcrypt encrypted passwords
This statement will yield a string in the correct format that you can
copy/paste into the repeatable migration using the format x'<hex
string here>'.
implementation("io.ktor:ktor-server-auth:2.1.2")
implementation("io.ktor:ktor-server-
sessions:2.1.2")
These plugins handle the session storage that your web app uses to
access information about the currently logged-in user and the ability to
compartmentalize routes in your web app to only be accessible to logged-in
users.
Next, you need to update your config files. You’ll add three different
properties: useSecureCookie (Boolean) will toggle whether your
session cookie will only work over HTTPS. cookieEncryptionKey
and cookieSigningKey (String) will set up the session cookie so
that it’s encrypted, which blocks hackers from forging a fake cookie.
Listing 9-17 shows what you need to add to your config files.
// app.conf
useSecureCookie = true
cookieEncryptionKey = ""
cookieSigningKey = ""
// app-local.conf
useSecureCookie = false
cookieEncryptionKey = "<ADD LATER>"
cookieSigningKey = "<ADD LATER>"
Listing 9-17 The default values of the various config files for the new config properties
fun Application.setUpKtorCookieSecurity(
appConfig: WebappConfig,
dataSource: DataSource
) {
}
Listing 9-18 The empty skeleton function setUpKtorCookieSecurity, where your
security config will reside
Later in this chapter, you’ll fill this function with your authentication
setup.
To make your Ktor web app use this security setup, remember to invoke
it in your call to embeddedServer where you already have your existing
call to createKtorApplication. Listing 9-19 shows how to call it
properly.
fun Application.setUpKtorCookieSecurity (
appConfig: WebappConfig,
dataSource: DataSource
) {
install(Sessions) {
cookie<UserSession>("user-session") {
transform(
SessionTransportTransformerEncrypt(
hex(appConfig.cookieEncryptionKey),
hex(appConfig.cookieSigningKey)
)
)
cookie.maxAge = Duration.parse("30d")
cookie.httpOnly = true
cookie.path = "/"
cookie.secure = appConfig.useSecureCookie
cookie.extensions["SameSite"] = "lax"
}
}
}
Listing 9-20 Adding setup for cookie-based sessions to setUpKtorCookieSecurity
Logging In
With authentication and session storage set up, you’re ready to add the
login form itself.
Your login form will be on a separate route, /login. It won’t be
looking pretty and stylish, but it will work. I’ll leave the prettifying up to
you. Listing 9-21 shows the code for the login page.
get("/login", webResponse {
HtmlWebResponse(AppLayout("Log in").apply {
pageBody {
form(method = FormMethod.post, action =
"/login") {
p {
label { +"E-mail" }
input(type = InputType.text, name =
"username")
}
p {
label { +"Password" }
input(type = InputType.password, name =
"password")
}
This login form is a straightforward HTML form that will post the
username and the password to another route. They’ll both have the path
/login, but a different verb. GET /login will display the login form,
and POST /login will perform the actual login and set of the session.
The next step is to implement the POST /login request to perform
the login when a user submits the form. Listing 9-22 shows the code for the
login handler.
post("/login") {
sessionOf(dataSource).use { dbSess ->
val params = call.receiveParameters()
val userId = authenticateUser(
dbSess,
params["username"]!!,
params["password"]!!
)
if (userId == null) {
call.respondRedirect("/login")
} else {
call.sessions.set(UserSession(userId =
userId))
call.respondRedirect("/secret")
}
}
}
Listing 9-22 The route handler inside setUpKtorCookieSecurity for handling the
submitted login form
install(Authentication) {
session<UserSession>("auth-session") {
validate { session ->
session
}
challenge {
call.respondRedirect("/login")
}
}
}
Listing 9-23 Adding setup for authentication to setUpKtorCookieSecurity
authenticate("auth-session") {
get("/secret", webResponseDb(dataSource) {
dbSess ->
val userSession = call.principal<UserSession>
()!!
val user = getUser(dbSess,
userSession.userId)!!
HtmlWebResponse(
AppLayout("Welcome, ${user.email}").apply {
pageBody {
h1 {
+"Hello there, ${user.email}"
}
p { +"You're logged in." }
p {
a(href = "/logout") { +"Log out" }
}
}
}
)
})
}
Listing 9-24 Wrapping a route in setUpKtorCookieSecurity to only be accessible after
users have logged in
Logging Out
The /secret route has a link to a route you haven’t implemented yet,
/logout. You’ll implement this route next, so that users can log out of
your web app.
A route for logging out should not do much other than performing the
logout and redirecting the user to a page that’s accessible for users who
haven’t logged in. In this example, you’ll redirect the user back to the
/login page. Listing 9-25 shows how to implement that route.
authenticate("auth-session") {
get("/logout") {
call.sessions.clear<UserSession>()
call.respondRedirect("/login")
}
// ...
Listing 9-25 Adding a route in setUpKtorCookieSecurity for logging out a user
As you can see from the code, you’ve wrapped the logout page in the
authenticate interceptor. That’s not important for security, but it
doesn’t really make sense to have the logout page available for users who
haven’t logged in.
The handler itself clears the session cookie and redirects back to the
login page. Note that it’s not actually possible to unset a cookie. The only
way to set a cookie is to use the Set-Cookie header and set it to
something. So Ktor and the session plugin set the cookie to "" (an empty
string). That causes subsequent requests that hit the authenticate
interceptor to treat the request as non-authenticated.
© The Author(s), under exclusive license to APress Media, LLC, part of Springer Nature 2023
A. Lilleaas, Pro Kotlin Web Apps from Scratch
https://doi.org/10.1007/978-1-4842-9057-6_10
In the modern world of web apps, it’s common to have back ends that are completely API
based and have either a mobile app or a JavaScript-based web front end that interacts with
them. In this chapter, you’ll learn how to set up an API for those scenarios.
If you’re new to Kotlin, here are some language features that you’ll see examples of in
this chapter:
Parsing and producing JSON with third-party libraries
Also in this chapter, you will learn the following about building API-based back ends:
Different types of authentication
How to securely set cookies for browsers
Building APIs for single-page web apps and native apps
Parsing Input
Most web apps receive inputs in some way or another, so you’ll need to know how to read
data that external parties post to your web handlers.
You’ve already read form-encoded data in Chapter 9, where you received form
parameters for the username and password for the form-based login in your HTML-based
web app setup. To do that, you used call.receiveParameters(), which parses the
HTTP request body with the form data encoding browsers use for HTML forms.
To read JSON, all you must do is to read the HTTP request body as a plain string and
parse it using the Gson library (https://github.com/google/gson) you’ve already
installed in Chapter 4 for writing JSON output via JsonWebResponse. The only extra step
you need to take for reading JSON compared with writing it is to specify the class that Gson
should serialize your data to – Map or List. Listing 10-1 shows an example of how to read
JSON in your web handlers.
post("/test_json", webResponse {
val input = Gson().fromJson(
call.receiveText(), Map::class.java
)
JsonWebResponse(mapOf(
"input" to input
))
})
Listing 10-1 Parsing JSON in your web handlers
All this web handler does is to parse the received JSON and return a response that re-
encodes the parsed JSON back to JSON.
The Ktor function call.receiveText() will handle encoding automatically and
parse the bytes submitted over HTTP to a string using either the encoding specified in the
HTTP headers of that request or the system-default encoding if the request didn’t specify
one.
post("/test_json", webResponse {
either<ValidationError,MyUser> {
val input = Gson().fromJson(
call.receiveText(), Map::class.java
)
MyUser(
email = validateEmail(input["email"]).bind(),
password = validatePassword(input["password"]).bind()
)
}.fold(
{ err ->
JsonWebResponse(
mapOf("error" to err.error),
statusCode = 422
)
},
{ user ->
// .. do something with `user`
JsonWebResponse(mapOf("success" to true))
}
)
})
Listing 10-2 Validating JSON input with Arrow
The use of Arrow in this code makes it look straightforward and natural, but at the same
time, Arrow’s use of kotlin.coroutines makes it quite clever. Either and bind are
both left biased, so as soon as a bound statement returns the left side of the Either (i.e., a
ValidationError), the execution of the code inside the either block stops at that
point, and that left side is the “winner.” If all the bound Either statements return the right
side, that value is simply returned as the result of those statements, and the right side with
MyUser is the “winner.”
fold is a function on Either that calls either a left or a right lambda, depending on
what the result of the Either was. fold is also a function that’s tagged with inline. If
you’re used to callbacks in a language like JavaScript or even Java, the code might look like
it just calls one of the two lambdas passed to fold, without returning anything. But because
fold is inline, you can think of the contents of the lambdas as if you wrote them at the top
level, outside of the fold function, and therefore the return values from the lambdas in fold
are what’s returned from the entire statement.
So, in short, this code either returns the error JsonWebResponse or the successful
JsonWebResponse, based on whether any of the validations inside the either block
returned a ValidationError.
@Serializable
data class MyThing(val myVal: String, val foo: Int)
obj.myVal
// "testing"
Json.encodeToString(obj)
// Returns {"myVal":"testing", "foo": 123}
Listing 10-3 Demonstrating kotlinx.serialization basics
Using kotlinx.serialization saves you from a lot of manual typing, as all you
need to do is to combine a data class with a JSON string and have Kotlin do the rest of the
work.
However, the major downside is that this tightly couples your public API to your internal
code base. If you change the name MyThing.myVal in your own code, you also break
callers to your own API.
Another downside is that your API entities are forced to follow Kotlin naming
conventions. In JSON, it’s common to use underscore_case and not camelCase for
property names. But since you’re doing a 1:1 serialization between dynamic data and typed
Kotlin data classes, they must be the same.
Note that this is heavily in the camp of opinionated best practice, and there are plenty of
smart and productive developers who will disagree with this proposition. But I consistently
make this distinction in all my production-grade web apps, to maintain flexibility and agility
in my own code while at the same time allowing stability and backward compatibility for
external callers of the API.
Single-Page Apps
A use case for APIs often seen in the wild is to target browsers and JavaScript-based single-
page apps (SPAs).
You should make sure to remove your existing static("/") route before you add the
singlePageApplication("/") route. The old static("/") route conflicts with
singlePageApplication("/"), which does everything your old static("/")
route did anyway.
The config properties in singlePageApplication are the same as those you use in
static. When running locally, you point to the files in the file system, whereas when running
in production, you point to the assets that your Gradle build compiles into the Java class path
of the compiled output.
The defaultPage points to the index.xhtml file that bootstraps your SPA. This file
should live in src/main/resources/public/index.xhtml. Typically, this file
only contains a bare-bones HTML file that loads the JavaScript that you use to load your
SPA. The reason it’s a HTML file, and not kotlinx.xhtml DSL code, is that you’ll likely
use some kind of SPA build tool such as Webpack to build your SPA, which often generates
its own HTML bootstrapping code that you must use. Additionally, the file should be
completely static and not change contents based on which route you’re looking at. So having
it as a static file works fine.
Keep in mind that to avoid conflicts between web pages and your API handlers, all your
API routes with /api/.... So, instead of logging in with a request to POST /login, you
should change that to POST /api/login. Then, make sure that your single-page app does
not have any pages that start with /api. That way, you’ll never have conflicting routes
between your Ktor back end and your JavaScript router.
implementation("io.ktor:ktor-server-cors-jvm:2.1.2")
install(CORS) {
allowMethod(HttpMethod.Put)
allowMethod(HttpMethod.Delete)
allowHost("localhost:4207")
allowHost("www.myapp.com", schemes = listOf("https"))
allowCredentials = true
}
Listing 10-5 Adding CORS setup to your Ktor app
The HTTP methods GET and POST are enabled by default. Most of my real-world web
apps also use at least PUT and DELETE, so you should allow those as well. You also need to
enable specific host names. CORS supports wildcard origins, but not when you want to
include credentials (i.e., cookies). Finally, you need to enable credentials in the first place, by
setting allowCredentials to true.
You also need to have a way to host your web app HTML locally. That’s why you’re
adding localhost:4207 to the list of allowed origins, which means that you can use the
singlePageApplication plugin in development and configure it to only load in the
local environment and not in production. Alternatively, you can use something like create-
react-app or set up Webpack yourself, to host an index.xhtml file and build your JavaScript.
If you use an external server like Webpack, update the port number to point to the port of
your Webpack server. (The intricate details of building a SPA are outside of the scope of this
chapter, so I’m deliberately skipping a lot of details on how to set it up.)
Authenticating Users
With this set up and ready to go, you can perform a request with your JavaScript to log in,
and it will work fine even if your JavaScript runs on a different origin than your API.
You can use the existing POST /login handler from Chapter 9 to log in from your
single-page app. That web handler expects form-encoded data, posted by a normal HTML
<form>. But you can generate that same type of request from JavaScript as well. Listing 10-
6 shows an example of how to do that.
fetch("/login", {
credentials: "include",
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded"
},
body: new URLSearchParams({
username: ...,
password: ...
})
})
Listing 10-6 Calling your API using JavaScript from a different origin
It’s important to remember to set credentials: "include" when you call fetch. If
you don’t, it won’t receive and send any cookies alongside your request.
Since the cookie is HTTP only, it’s impossible to access it from JavaScript. That means
that if you for some reason become the victim of a script injection attack, there’s no way for
the attacker to extract session keys, which is a big win for security.
If you’re making a pure API without any HTML, you could also change POST /login
to receive and parse JSON instead of data that’s like what you’d get in a HTML <form> tag.
If you do that, you should also change your API to return a 200 OK instead of a redirect upon
successful login.
Native Apps
Non-web native apps, such as mobile apps and native desktop apps, can also call your API.
This category also includes web-based wrappers, where a native app wraps web technologies,
such as Electron. Even if those apps use web technologies, they don’t have to succumb to the
security sandbox of browsers and can perform network calls just like plain native apps.
implementation("io.ktor:ktor-server-auth-jwt-jvm:2.1.2")
The next step is to set up your JWT authentication. You’ll do that in a new Ktor
Application extension function, like your existing functions createKtorApplication
and setUpKtorCookieSecurity. Listing 10-7 shows how to set that up.
fun Application.setUpKtorJwtSecurity(
appConfig: WebappConfig,
dataSource: DataSource
) {
val jwtAudience = "myApp"
val jwtIssuer = "http://0.0.0.0:4207"
authentication {
jwt("jwt-auth") {
realm = "myApp"
verifier(JWT
.require(Algorithm.HMAC256(appConfig.cookieSigningKey))
.withAudience(jwtAudience)
.withIssuer(jwtIssuer)
.build())
validate { credential ->
if (credential.payload.audience.contains(jwtAudience))
JWTPrincipal(credential.payload)
else
null
}
}
}
}
Listing 10-7 Setting up JWT authentication
To simplify the setup, you’re reusing the config property cookieSigningKey for the
JWT signing key. You can add a separate config property for the JWT only if you want.
The authentication process itself does not use the issuer and audience properties. They’re
only there for verification purposes, which can be useful if your API can accept JWTs from
various sources or if you want to issue JWTs that you can use in different authentication
scopes.
The next step is to add a web handler for performing the actual logging in and creation of
new JWTs. Listing 10-8 shows how to set that up.
routing {
post("/login", webResponseDb(dataSource) { dbSess ->
val input = Gson().fromJson(
call.receiveText(), Map::class.java
)
val userId = authenticateUser(
dbSess,
input["username"] as String,
input["password"] as String
)
if (userId == null) {
JsonWebResponse(
mapOf("error" to "Invalid username and/or password"),
statusCode = 403
)
} else {
val token = JWT.create()
.withAudience(jwtAudience)
.withIssuer(jwtIssuer)
.withClaim("userId", userId)
.withExpiresAt(
Date.from(LocalDateTime
.now()
.plusDays(30)
.toInstant(ZoneOffset.UTC)
)
)
.sign(Algorithm.HMAC256(appConfig.cookieSigningKey))
JsonWebResponse(mapOf("token" to token))
}
})
}
Listing 10-8 Performing a login inside the setUpKtorJwtSecurity configuration
authenticate("jwt-auth") {
get("/secret", webResponseDb(dataSource) { dbSess ->
val userSession = call.principal<JWTPrincipal>()!!
val userId = userSession.getClaim("userId", Long::class)!!
val user = getUser(dbSess, userId)!!
JsonWebResponse(mapOf("hello" to user.email))
})
}
Listing 10-9 Securing routes inside the routing block with JWT authentication
Instead of using the custom principal data class you set up for cookie-based auth, you use
the built-in JWTPrincipal class that represents JWTs. This principal is set up to parse the
correct headers automatically and decode the token.
The JWT contains the userId claim you specified when you created the token on POST
/login.
This code has lots of potential for null pointer exceptions in it. There are three !!
operators, which cause your code to throw a NullPointerException if the value is
null. I prefer to keep the type system as clean as possible and be explicit about the types I
require in my business logic and whether they’re nullable. But for code at the edges of the
system, where all sorts of weird things can happen based on user input, I don’t mind using
the !! operator to fail early and sanitize input. It’s a cheap form of input validation, which
generates bad error messages, but it is really easy to add and is much better than passing
nullable types all over your code.
Performing Authentication
To demonstrate how you’ll authenticate against this API, you’ll use cURL in a terminal to
perform API calls.
First, make sure your API is ready to go with JWT-based authentication. In
embeddedServer, remove the existing call to setUpKtorCookieSecurity, and
replace it with your new function from this chapter, setUpKtorJwtSecurity, and run
your main function to start up your web app.
You’ll get the JSON you generated in your code in Listing 10-8 in return. First, try to call
GET /secret without an auth token, to see what happens:
$ curl -I http://localhost:4207/secret
To perform authentication, you’ll call POST /login with the username and password
from the user in your repeatable migration:
$ curl -X POST \
-d '{"username":"[email protected]","password":"1234"}'
\
http://localhost:4207/login
{"token":"<token here>"}
Next, perform a request to GET /secret and pass in the auth token you got, and you
should get a successful result in return:
{"hello":"[email protected]"}
If you did everything correctly, you got the email of the user that the JWT represents.
Note the format of the Authorization header. It starts with the text "Bearer",
followed by the actual token. This is a part of the HTTP specification, as Authorization
headers should first state the authentication method (in this case, a JWT bearer) and then the
actual authorization parameters (the JWT itself). You won’t be able to authenticate
successfully without the "Bearer " prefix.
© The Author(s), under exclusive license to APress Media, LLC, part of Springer Nature 2023
A. Lilleaas, Pro Kotlin Web Apps from Scratch
https://doi.org/10.1007/978-1-4842-9057-6_11
The most common way to deploy production-grade web apps today is in a traditional
server-based environment. This chapter covers how to package your web app so you
can deploy it in several types of environments, be it a single-node dedicated server or a
distributed multi-server Kubernetes cluster.
Additionally, the JAR file will contain the file META-INF/MANIFEST.MF, which is
a text file that contains some metadata about the build and the line Main-Class, which
points to the specific class that the Java runtime should load and invoke the main
function on.
plugins {
id("com.github.johnrengelman.shadow") version "7.1.2"
}
Note that you’re not just adding a dependency this time. In fact, you’re not adding a
dependency at all, and you should leave the dependencies block unchanged.
You’re installing a plugin and configuring a task for it.
You already have a plugins block in build.gradle.kts that installs the Kotlin
plugin itself, so you can add the Shadow plugin to the existing plugins block instead of
duplicating the plugins block statement. It will work either way, though.
The Shadow plugin needs to know which class it should execute the main function
on, so that it can write the correct main class name to META-INF/MANIFEST.MF in the
generated JAR file. You don’t have to add any additional configuration for this, though,
as Shadow piggybacks on the plain JAR file that Gradle already knows how to build
and that JAR file gets the main class name from the application plugin that you’ve
already configured back in Chapter 1.
When the shadowJar task has finished, your fat jar file is available in
build/libs/kotlinbook-1.0-SNAPSHOT-all.jar. This file contains all your own code, as
well as all the third-party dependencies you’ve specified in build.gradle.kts.
For the local environment, you’ve hard-coded default values for all the config
properties in your system. But the secrets for your production environment should be
secret, as explained in Chapter 3. So, in production mode, your web app expects that
several environment variables are set. Typesafe Config will fail early, so it crashes
immediately when it tries to load app-production.conf but is unable to find a value for
the environment variables that you’ve specified that it should load its config values
from.
FROM amazoncorretto:17.0.4
That’s all there is to it. The first line says which Docker image you want to inherit
from. amazoncoretto:17.0.4 is an image that the Amazon teams provide, which
includes a full Linux distribution, and comes preinstalled with Java 17 Corretto, the
same version of Java you’ve used to run the code you’ve written in this book. You can
see all the Amazon Corretto Java versions available on the Docker Hub page for the
amazoncoretto image at
https://hub.docker.com/_/amazoncorretto.
There are many other Docker base images available that you can use instead, for
different versions and builds of Java. For example, the Zulu Java distributions by Azure
and Microsoft are available at https://hub.docker.com/r/azul/zulu-
openjdk. Open JDK’s Alpine Linux–based images
(https://hub.docker.com/_/openjdk) used to be popular choices, but you
should avoid them as Open JDK has deprecated their own images and won’t release
any updates. There are other vendor-neutral alternatives available now, such as the
Eclipse Temurin builds (https://hub.docker.com/_/eclipse-temurin).
You set up the Docker image to create a folder /app and to copy the fat jar that you
build with shadowJar from your machine and into the Docker image container.
Then, you use CMD to specify exactly how to load your web app. You can use the CMD
to specify additional flags to the Java command too, such as flags to configure the
memory limits, which garbage collector to use, JMX setup, and so on.
To build the Docker image, run docker build:
This builds a Docker image into the local Docker repository. -f specifies the path
to the Dockerfile. -t specifies tags. You can have as many tags as you like. By
convention, myimagename:latest is how you point to the latest version of an
image called myimagename, so you should always include that tag. Finally, . points
to the working directory, which is how docker build knows where to find the files
you tell it to COPY.
Note that as your application grows, and your Docker build commands start taking
too long, you can split up the Docker image into layers. For example, you could have
your external dependencies in one layer and your own code in another. That way, your
production environment doesn’t have to re-download your third-party dependencies
every time you deploy the latest version, as that layer is unchanged and already cached
on the production servers. This is not something I’ve needed to do in a real-world web
app yet, but it’s worth mentioning regardless. You can investigate using a tool like Jib
from Google (https://github.com/GoogleContainerTools/jib) instead
of the Shadow plugin if you need to create layered Docker images.
The -p flag specifies port mapping. Here, you say that you want the port 9000 on
your machine to bind to the port 4207 inside the Docker image. Then, you name the
running Docker image kotlinbook. And the Docker image you want to run is
kotlinbook:latest, which refers to the tag you used when you built the Docker
image using docker build earlier.
When you run this command, you’ll see all the console output of the Java -jar
command that the Docker image executes. And if you open your browser on
http://localhost:9000, you’ll see your web app up and running!
Just like when you ran it directly on your machine using java -jar, your web
app starts up in the default mode, which is in the local environment, which causes it to
load your development default configuration values in app-local.conf.
You can also pass the additional -d flag to docker run to make Docker run the
image in the background, instead of taking up the entire terminal and displaying all the
log output.
If you want to rebuild and rerun your Docker image, you’ll have to remove the
named kotlinbook image first by running the following command:
docker rm -f kotlinbook
If you try to run docker run again without first removing the kotlinbook
image, you’ll get an error message saying that an image named kotlinbook already
exists.
Deploying to Production
When you have a Docker image that you can run, the next step is to use that Docker
image and run it in production.
Explaining how to set up a fully working production environment is outside of the
scope of this book. Instead, I’ll give you a general overview of the pieces you need that
are common to all production environments.
The most common way to run Docker images in production on your own servers is
to use Docker Swarm (https://dockerswarm.rocks/), Kubernetes
(https://kubernetes.io/), or Apache Mesos
(https://mesos.apache.org/). All these solutions can run multiple instances
in parallel, with load balancers in front that direct traffic dynamically to your multiple
instances, automatically kill unhealthy instances, and so on.
You need to set a collection of environment variables for your production setup.
You’ll need to set KOTLINBOOK_ENV to production so that your web app loads
the environment variable–based app-production.conf setup. You can set
KOTLINBOOK_HTTP_PORT, if you want to use a different HTTP port than the default
4207 from app.conf. Then, you’ll need to update app-production.conf so that it points
to environment variables for all your config properties, instead of pointing to the
default null values in app.conf. For example, dbUrl should point to an environment
variable like KOTLINBOOK_DB_URL, dbPassword should point to
KOTLINBOOK_DB_PASSWORD, and so on.
You also need some way of setting environment variables and managing secrets. If
you use Kubernetes, you can use Helm and Helm Secrets
(https://github.com/jkroepke/helm-secrets) to encrypt secrets at rest
and manage environment variables in your Kubernetes cluster.
You’ll also need a Docker image registry. For example, if you use managed
Kubernetes on Azure, you’ll get access to a managed registry at myapp.azurecr.io.
When you build Docker images that you’ll deploy in Kubernetes, you’ll run docker
push kotlinbook:latest, where the name kotlinbook:latest refers to a
tag that you assigned when you ran docker build. You’ll then configure your
Kubernetes cluster to pull images from the Azure container registry at myapp.azurecr.io
and always deploy the tag kotlinbook:latest when you run kubectl
rollout restart to recreate your running pods.
If you don’t need a multi-node cluster setup, you can also use Docker to run a
single instance of an image on a single virtual or physical server. If you SSH into your
server and start up your Docker image using the command docker run –
restart=always -d kotlinbook, Docker will automatically start your Docker
image when your server reboots. This also restarts if the java -jar process you start
with the CMD in your Dockerfile crashes.
© The Author(s), under exclusive license to APress Media, LLC, part of Springer Nature 2023
A. Lilleaas, Pro Kotlin Web Apps from Scratch
https://doi.org/10.1007/978-1-4842-9057-6_12
In this chapter, you’ll learn how to take your existing handlers that you currently run in Ktor and invoke them in a
serverless environment, without Ktor.
In this chapter, you’ll learn the following about building and deploying to a serverless environment:
Setting Java runtime flags to optimize startup time
Intricate details about the Java runtime class loading and performance characteristics
How AWS Lambda executes your code
Important cost-saving tips in serverless environments
An important disclaimer about this whole chapter is that I have extensive experience with serverless
architectures using AWS and Node.js. And I have extensive experience with running Java platform web apps in
production. But I’ve never actually deployed a Java-based web app to a serverless environment. So keep that in
mind, and use this chapter as a guide to get started and not distilled advice from a seasoned Java + serverless
expert.
get("/foo", webResponse {
// Do something ...
JsonWebResponse(mapOf("success" to true))
})
There’s one notable exception, though. In Chapter 8, you wrote handleCoroutineTest as a standalone
function that you call from a Ktor handler:
Basic Structure
The structure I usually follow is that my Main.kt does all the bootstrapping, loading of config files, and starting
of servers, just like in this book. It also contains all the web handler route declarations in a single file, but not the
implementation of those routes. So it typically looks something like this:
fun main() {
val config = createConfig(...)
val dataSource = createAndMigrateDataSource(config)
val stuff = createStuff(config)
fun Application.createKtorApplication(
dataSource: DataSource
) {
routing {
get("/foo", webResponse(::handleGetFoo))
get("/bars", webResponseDb(dataSource, ::handleGetBars))
get("/bars/{id}", webResponseDb(dataSource) { dbSess ->
handleGetBar(dbSess, call.parameters["id"])
})
post("/bars", webResponseTx(dataSource) { txSess ->
handleCreateBar(
txSess,
Gson().fromJson(call.receiveText(), Map::class.java)
)
})
// ...
}
}
The various handler functions have no knowledge of Ktor and only work on a combination of plain data such
as a Map from JSON parsing and a String corresponding to the ID path parameter from Ktor.
Having all the routes in a single file makes it easier to read the code. The process of debugging a web app is
often that you see a request that’s made to some URL and the first thing you need to figure out is which code that
runs when a given URL is invoked against a web app.
For this reason, I also tend to avoid using what I call “path prefix wrappers”, no matter if I use Ktor or another
routing library (or programming language) entirely. For example, Ktor allows you to wrap routes with a path prefix
like this:
routing {
get("/foo", webResponse(::handleGetFoo))
route("/api") {
get("/version", webResponse(::handleGetApiVersion))
post("/order", webResponseTx(dataSource) { txSess ->
handleCreateOrder(txSess, call.receiveText())
})
}
}
This works fine and isn’t a problem if you have a small collection of routes. But as your web app grows, it’s a
time saver to be able to search for "/api/order" in your project and find the code that corresponds to that path
instantly. So I prefer the boilerplate of stating the full path of all my web handlers:
routing {
get("/foo", webResponse(::handleGetFoo))
get("/api/version", webResponse(::handleGetApiVersion))
post("/api/order", webResponseTx(dataSource) { txSess ->
handleCreateOrder(txSess, call.receiveText())
})
}
Here, all the paths that your web app supports are present in the code base, making your code more explicit and
readable, at the cost of having to manually repeat nested path segments.
implementation("com.amazonaws:aws-lambda-java-core:1.2.1")
This library contains the necessary wrapping code to write Kotlin code that AWS Lambda can invoke. The
essence of AWS Lambda and the Java platform is that you package your code as a JAR file the same way you
package for a traditional server-based environment. Then, your JAR file contains special classes that represent
individual lambda handlers. These classes inherit from
com.amazonaws.services.lambda.runtime.RequestHandler, which gives them a signature that
AWS Lambda knows how to invoke and process the return value of.
To separate your existing code from the AWS Lambda environment, you’ll create a new file,
MainServerless.kt. In this file, you’ll include all your AWS Lambda handlers, as well as the code needed to
bootstrap your code to run in the AWS Lambda environment. Listing 12-1 shows how to do that.
import com.amazonaws.services.lambda.runtime.Context
import com.amazonaws.services.lambda.runtime.RequestHandler
class GetTestHelloWorld :
RequestHandler<Any?, String>
{
override fun handleRequest(
input: Any?,
context: Context
): String {
return Gson().toJson(mapOf(
"statusCode" to 200,
"headers" to mapOf("contentType" to "text/plain"),
"body" to "Hello, World!"
))
}
}
Listing 12-1 Setting up MainServerless.kt with basic AWS Lambda handlers
Your AWS Lambda handler class implements the RequestHandler interface and returns a String with
JSON content. AWS Lambda itself does not do any validation of its output. The JSON format you use in
GetTestHelloWorld is the format that AWS API Gateway expects when it invokes lambdas to handle web
requests. So, if you set up API Gateway to invoke your lambdas, you’ll get errors from API Gateway if you forget
to include a "statusCode" or return non-JSON output. But later, when you’ll invoke your lambda handlers
manually to test them, there’s no requirement on the output format of your lambda handlers.
Click the big orange button labeled “Create function.” That takes you to the page seen in Figure 12-2.
Figure 12-2 Creating a new function in the AWS Lambda console
Leave the option “Author from scratch” selected, name your function kotlinbookTestFunction, choose
“Java 11 (Corretto)” as the runtime, and click the “Create function” button at the bottom of the screen.
When AWS has finished creating your function, you need to upload your code. You’ll upload the entire fat jar
that you assembled with the shadowJar Gradle task earlier. Click the white button labeled “Upload from,” and
click the option labeled “.zip or .jar file,” as seen in Figure 12-3.
Figure 12-3 Uploading your code to your new AWS Lambda function
In the popup that appears, click the white button labeled “Upload,” choose your fat jar file in the file finder
window that appears, and click the orange button labeled “Save”, as seen in Figure 12-4.
Note that the location of the fat jar file that you should upload to AWS Lambda is in the folder
build/libs/kotlinbook-1.0-SNAPSHOT-all.jar alongside your source code and Gradle configuration in your web app
project.
Figure 12-4 Choosing a file and performing the upload in AWS Lambda
When your browser has finished uploading that file to AWS Lambda, you’re returned to the same screen as
you saw in Figure 12-3. Remain on that screen and scroll down a bit. You’ll see a section named “Runtime
settings.” Click the white “Edit” button, as seen in Figure 12-5.
Figure 12-5 Editing the runtime settings of your AWS Lambda function
In the popup that appears, you need to change the name of the class that AWS Lambda will invoke. It’s
currently set to example.Hello, which is not a class that exists in your web app. You need to change it to the
name of your actual handler class, kotlinbook.GetTestHelloWorld, as seen in Figure 12-6. The function
name handleRequest should stay the same, as that’s what you’ve named that function in the code that you just
uploaded. Click the big orange button labeled “Save” to continue.
Figure 12-6 Pointing AWS Lambda to the correct handler class in your code
You’re not ready to invoke your function, to see if everything works correctly! Scroll up to the tabs, where
“Code” is the tab that’s currently activated, and choose the “Test” tab. On that page, click the big orange button
labeled “Test.” In the following, you can change the parameters passed to your lambda as well. But your test
handler does not do anything with the input AWS Lambda passes to it, so you don’t have to change anything there.
Figure 12-7 shows what this looks like.
Figure 12-7 You’ve successfully tested your function!
If you click “Details” in the green box in Figure 12-7, you’ll see more information about the execution of your
AWS Lambda handler, as seen in Figure 12-8.
Figure 12-8 Detailed info about the execution of your AWS Lambda handler
There’s the output from your code! As previously mentioned, AWS Lambda itself does not interpret the output
in any way; it just forwards it to the caller, which in this case was the AWS Lambda testing console. It’s up to the
caller to interpret it, and if you set up API Gateway to call your lambdas, it will use this JSON output to determine
how to respond to incoming HTTP requests.
handleUserEmailSearch(dbSess, input["email"])
})
fun handleUserEmailSearch(
dbSess: Session,
email: Any?
): WebResponse {
return JsonWebResponse(dbSess.single(
queryOf(
"SELECT count(*) c FROM user_t WHERE email LIKE ?",
"%${email}%"
),
::mapFromRow)
)
}
Listing 12-2 Creating a reusable Ktor handler
Here, you’ve created a Ktor handler that parses the JSON input on the Ktor side of things and passes a database
session and the username property from the parsed JSON into a stand-alone function.
Next, you’ll invoke this stand-alone function, handleUserEmailSearch, from AWS Lambda. The first
thing you’ll need is a way to map your WebResponse data to AWS Lambda. Listing 12-3 shows how to do that.
fun getAwsLambdaResponse(
contentType: String,
rawWebResponse: WebResponse,
body: String
): String {
return rawWebResponse.header("content-type", contentType)
.let { webResponse ->
Gson().toJson(mapOf(
"statusCode" to webResponse.statusCode,
"headers" to webResponse.headers,
"body" to body
))
}
}
fun serverlessWebResponse(
handler: suspend () -> WebResponse
): String {
return runBlocking {
val webResponse = handler()
when (webResponse) {
is TextWebResponse -> {
getAwsLambdaResponse(
"text/plain; charset=UTF-8",
webResponse,
webResponse.body
)
}
is JsonWebResponse -> {
getAwsLambdaResponse(
"application/json; charset=UTF-8",
webResponse,
Gson().toJson(webResponse.body)
)
}
is HtmlWebResponse -> {
getAwsLambdaResponse(
"text/html; charset=UTF-8",
webResponse,
buildString {
appendHTML().xhtml {
with(webResponse.body) { apply() }
}
}
)
}
}
}
}
fun serverlessWebResponseDb(
dataSource: DataSource,
handler: suspend (dbSess: Session) -> WebResponse)
= serverlessWebResponse {
sessionOf(
dataSource,
returnGeneratedKey = true
).use { dbSess ->
handler(dbSess)
}
}
Listing 12-3 Mapping WebResponse to AWS Lambda
The function serverlessWebResponse is slightly overengineered for your current demands. It supports
coroutines in your handler functions. The function you just wrote, handleUserEmailSearch, does not use
coroutines. But since it’s likely that your real-world handlers will include coroutines, the mapping code supports
them, so make it as useful as possible in real-world scenarios.
serverlessWebResponse takes a WebResponse and converts it into AWS API Gateway–compatible
JSON. TextWebResponse and JsonWebResponse are straightforward. The most complicated conversion is
for HtmlWebResponse, as it must take kotlinx.xhtml DSL code and convert it into a String.
Thankfully, kotlinx.xhtml has some convenient helper functions that make the job easy. buildString in
Listing 12-3 comes from the Kotlin standard library and is a convenience API around the Java platform
StringBuilder API for taking chunks of strings and bytes and converting them into a single big String.
appendHTML is from kotlinx.xhtml and expects an Appender, which a StringBuilder is, and
appends its HTML to it in chunks. That way, kotlinx.xhtml doesn’t have to create the entire HTML string first and
then return it to the caller. Finally, you’re using the same trick we’ve used before to invoke apply() on the
webResponse.body object using with, to avoid calling the built-in scope function apply.
Note that you could have implemented getAwsLambdaResponse as a single-expression function, that is, a
function with an = after the name, which doesn’t have to specify the return type and so on. But the value you
return is the result of Gson().toString(), which is a Java class that returns a platform type String!. So, to
avoid passing around platform types in your code, you explicitly state that it’s a String to make Kotlin able to
better work its logic for automatically detecting potential null pointer exceptions at compile time.
To create the actual AWS Lambda handler function, you’ll also need a database connection. Listing 12-4 shows
how to set both up correctly.
Your database initialization code just sets up H2 directly and migrates that H2 database using your Flyway
(https://flywaydb.org/) migrations. You could use your config file system to configure H2 if you wanted
to, but in the world of serverless and AWS Lambda, it’s more common to use environment variables that are
specific to that handler, instead of using config files that are baked in alongside your code. So, to make the code
more realistic, you’ve skipped WebappConfig entirely.
The RequestHandler type of this lambda is slightly different, as it takes a Map<String, String>
instead of Any?. The actual input you’ll get will always be of the type Map<String, String>, so using
Any? before in your GetTestHelloWorld handler in Listing 12-1 was just a shortcut to save you some typing,
as you didn’t actually use the input parameters there.
Rebuild your code with the Gradle shadowJar task, upload the new JAR file to the AWS Lambda console as
described in Figure 12-3, and change the name of the handler class from
kotlinbook.GetTestHelloWorld::handleRequest to
kotlinbook.UserEmailSearch::handleRequest as shown in Figure 12-5. Before you run your code
by hitting the orange button labeled “Test” under the “Test” tab shown in Figure 12-7, remember to update the
parameters. Set the key "email" to a value that matches the user that you insert in your repeatable migration, as
seen in Figure 12-9.
Figure 12-9 Specifying input parameters when running your AWS Lambda function
If everything worked correctly, you should get a green box exactly like the one you got in Figure 12-7 when
you ran your AWS Lambda function the first time, and if you expand the “Details” section of the green box, you’ll
should see the JSON output with the result of your database query.
Improving Performance
There are many things you can do to improve the performance of your AWS Lambda handlers, which will help
with both cost-saving and reducing latency and execution times of your web app.
Lazy Loading
There’s a red flag in the timings you received when you executed GetTestHelloWorld. For the cold start
invocation, the initialization time was 444.01 ms, and the execution time was 378.68 ms. On subsequent
invocations, there was no initialization time, as the serverless runtime already had your code loaded, but the
execution time was only 2–3 ms.
Why is the execution time so different on the cold start compared with subsequent warm start invocations?
This is hard evidence that there’s initialization happening in your handler function. The first time you execute
your handler function, there’s something that the serverless runtime for some reason hasn’t fully loaded.
The reason for this is that the Java runtime lazily initializes all classes. And that’s a good thing. Your JAR file
contains all of Ktor as well as the entirety of the dependencies that you’ve specified in build.gradle.kts. The Java
runtime only parses and loads the compiled class files when they’re used by running code. Without this feature,
you would soon hit the 10-second hard limit on initialization time on AWS Lambda, as loading and parsing all that
code is a lot of work, which the Java runtime skips because of the lazy loading.
But this has the side effect of causing the serverless runtime to load all the dependencies of your lambda
classes during execution, not initialization! All the code in H2, HikariCP, Main.kt, and so on loads during the
execution phase, as that’s the first time running code that references those classes.
This also applies to your top-level private val dataSource declaration in UserEmailSearch. It
might look like this code runs statically when you look at your Kotlin code. But because of implementation details
in the way the Kotlin compiler writes Java class files, it doesn’t. Under the hood, Kotlin compiles the top-level
declarations in MainServerless.kt to a Java class named MainServerlessKt. That class has a static
initialization block that contains the code to initialize the dataSource property. But the first time your code
refers to that class is inside the handleRequest function. Kotlin compiles dataSource to
MainServerlessKt.getDataSource(). When the Java runtime encounters that call, it registers that it
hasn’t loaded MainServerlessKt yet. So it loads that class, causing the corresponding static block to run. But
that’s too late – you’re in the execution phase by that time, not in the initialization phase.
Initializing Correctly
To fix this, you need to somehow invoke code in MainServerlessKt during the initialization phase, so that the
Java runtime loads that class during initialization and not during the first cold start execution.
Before optimizing, I got an initialization time of 354.8 ms and an execution time of 5678.15 ms. Subsequent
invocations only took around 3–4 ms. This is really bad and indicates that almost nothing happens during the
initialization phase and that this code needs a lot of initialization time to work, as it spends almost 6 seconds
initializing itself on the first call.
The long initialization time makes sense. GetTestHelloWorld does almost no work, so it doesn’t take as
long to initialize. But UserEmailSearch loads Gson, your WebResponse handling code, HikariCP, Flyway,
and the entirety of H2, which is a whole embedded database engine.
The easiest way to do fix this is to add a companion object with an initialization block to your
UserEmailSearch handler class. The Kotlin compiler compiles that init block to a static initialization
block in the underlying UserEmailSearch Java class. This static initialization block will run during the
initialization phase of your AWS Lambda handler runtime, because UserEmailSearch is the class you specify
that AWS Lambda should execute to run your code. Listing 12-5 shows how to update your handler with static
initialization.
class UserEmailSearch :
RequestHandler<Map<String, String>, String>
{
companion object {
init {
runBlocking {
serverlessWebResponseDb(dataSource) { dbSess ->
dbSess.single(queryOf("SELECT 1"), ::mapFromRow)
JsonWebResponse("")
}
}
}
}
The init block does as much as possible to match what the actual handler code does. This is to ensure that
the Java runtime loads as many of the classes you use during execution as possible. It initializes the Kotlin
coroutine classes with runBlocking, it initializes the whole WebResponse handling code, and it runs an
actual dummy query against H2, causing all database-related code to load as well.
With this small tweak, I now got an initialization time of 1298.85 ms, up from 354.8 ms, but an execution time
of just 22.12 ms, down from 5678.15 ms. That’s a big win! You’re now down to a total of 1320.77 ms on a cold
start, compared with 6032.95 ms from before. So, instead of AWS Lambda billing you for 5679 ms of execution
handler time that included loading of all the code running in that lambda, you’re now only billed for 23 ms of
execution time for your handler code.
If you compare the execution time of the first call, 22.12 ms, with subsequent warm invocations, you can see
that there’s still a difference. Subsequent warm calls still only take 3–4 ms to complete. So there’s still at least
some initialization going on during the initial execution. But 23 ms is a low enough number that it’s not worth
going down the rabbit hole of figuring out what could lower that number even further.
Migrations and H2
Note that one of the things that causes your initialization time to be almost 1300 ms, is that you’re loading in all
H2, a full database engine, and you’re also running your Flyway migrations, on every cold start.
This is an unrealistic scenario for a real-world web app. In the world of serverless, it’s common to not even use
a connection pool library like HikariCP, because a serverless function should never have to manage multiple
simultaneous connections. In fact, if you use a serverless database such as AWS Aurora
(https://aws.amazon.com/rds/aurora/), you can run queries against it using HTTP requests, so that
you don’t have to manage any connections at all and instead run low-latency fire-and-forget queries.
Additionally, if you use an external SQL database from your serverless handlers, you wouldn’t run the
migrations on every cold start. You wouldn’t even include the migrations in the JAR file you build for your
serverless execution environment. Instead, you would either run the database migrations in your build pipeline or
create a dedicated serverless function that runs your migrations that your build pipeline invokes when it deploys a
new version of your code.
Java Runtime Flags
To further optimize performance, you can investigate the world of setting Java runtime compiler flags.
For example, you can set the environment variable JAVA_TOOL_OPTIONS to -
XX:+TieredCompilation -XX:TieredStopAtLevel=1, which disables tiered compilation. Tiered
compilation is a feature where the Java runtime analyzes your code as it’s running. The runtime optimizes your
code on the fly, based on the result of the analysis. This is a part of the just-in-time (JIT) compiler in the Java
platform, where the Java runtime dynamically compiles Java bytecode into native machine code.
Tiered compilation makes sense for traditional server-based processes that run for hours or days without
shutting down but might not make sense in an environment where fast start times are important. When I tested this
using the lambda you wrote in this chapter, initialization duration went form around 450 ms to consistently only
spending about 330 ms. But this book is not about optimizing the Java runtime performance, so I won’t go into
further detail on that front.
In this chapter, you’ll learn how to use Spring Context for managing
resources in your web app and wire components together using dependency
injection.
If you’re new to Kotlin, here are some language features that you’ll see
examples of in this chapter:
Null safety with lateinit
Using lambdas in place of object literals
Also in this chapter, you will learn the following about using Spring
Context:
Programmatically creating and configuring Spring Context – no XML!
Setting up lazy beans
Initializing Spring Context to fail early
Gracefully cleaning up all your resources on shutdown
In this chapter, you’ll learn how to write up the resources in your web
app using dependency injection instead of explicit initialization, using
Spring Context, the popular and ubiquitous Java dependency injection
library.
implementation("org.springframework:spring-
context:5.3.23")
You’ll use Spring Context to initialize your web app. To avoid conflicts
with your existing code, you’ll create a new main file,
MainSpringContext.kt, to separate your Spring initialization from your
existing initialization in Main.kt. Spring Context will initialize the
components and resources of your web app in the correct order, based on the
dependency graph Spring Context detects between them. So you won’t need
any of the “manual” initialization code already present in Main.kt.
At the core of Spring Context is the application context object. This is
often a resource that’s created via XML config files. But here, you’ll set it up
programmatically. Listing 13-1 shows how to get the basics up and running.
fun createApplicationContext(appConfig:
WebappConfig) =
StaticApplicationContext().apply {
beanFactory.registerSingleton("appConfig",
appConfig)
refresh()
registerShutdownHook()
}
Listing 13-1 Creating and initializing Spring Context
Here, you’ve registered a bean the traditional way. It’s not a singleton, so
you tell Spring Context how to create an instance of a class you provide and
various attributes of the instance that’s created. In this case, you say that the
properties jdbcUrl, username, and password are to be set to the
corresponding values in your WebappConfig.
If you ask Spring Context for an instance of
"unmigratedDataSource", you’ll get just what the name suggests – a
raw HikariCP data source that you can use to query your database. But it
won’t run any pending Flyway (https://flywaydb.org/) database
migrations before you start executing queries. Listing 13-3 shows how to set
up an additional bean that also runs your migrations.
class MigratedDataSourceFactoryBean :
FactoryBean<DataSource> {
lateinit var unmigratedDataSource: DataSource
fun main() {
log.debug("Starting application...")
val env = System.getenv("KOTLINBOOK_ENV") ?:
"local"
Extended Usage
What about extended usage of Spring Context? For example, all your web
handlers now operate exactly as before, by taking the parameters they use in
as arguments, in plain functional style.
You could write a web handler as a bean like this:
beanFactory.registerSingleton(
"springContextTestHandler",
webResponse {
TextWebResponse("Hello from Spring!")
}
)
Then, you could add a route mapping for that handler like this:
get(
"/spring_context_test",
ctx.getBean("springContextTestHandler") as
PipelineInterceptor<Unit, ApplicationCall>
)
The problem with this solution is that you haven’t really added any extra
value, other than wrapping lambdas in Spring Context instead of inserting
the lambda directly. There’s no way to inject dependencies into a lambda.
You could go down the route of making factory holder beans and other
noun-ridden constructs, but I tend to stay away from that sort of thing.
Dependency injection adds value for initialization and teardown of
resources, but for the implementation of web route handler functions, the
only value dependency injection adds is that it adds some extra ceremony to
the simple task of passing arguments to functions.
© The Author(s), under exclusive license to APress Media, LLC, part of Springer Nature 2023
A. Lilleaas, Pro Kotlin Web Apps from Scratch
https://doi.org/10.1007/978-1-4842-9057-6_14
In this chapter, you’ll learn how to bridge the gap between the old and the new and add Spring Security
(https://spring.io/projects/spring-security) to your Ktor-based web app.
If your organization is already widely using Spring Security, you might already have lots of infrastructure in
place that would take heroic efforts of breaking Conway’s law to change. Instead, you can add Spring Security to
your modern Kotlin web app and leverage your organization’s existing plugins and expertise.
Note that Spring Security is not meant to compose. Spring Security wraps your entire web app, and if you
already have something else set up for authentication, such as the cookie-based authentication from Chapter 9 or a
third-party system such as Keycloak (www.keycloak.org/), they will conflict and step on each other’s toes.
implementation("io.ktor:ktor-server-servlet:2.1.2")
implementation("org.eclipse.jetty:jetty-server:9.4.49.v20220914")
implementation("org.eclipse.jetty:jetty-servlet:9.4.49.v20220914")
You’ll use Jetty for the actual server and ktor-server-servlet for mapping your existing Ktor app to
the Jetty servlet setup.
The latest version of Jetty is 11. The reason you’re using the latest version of v9 is that the later version of Jetty
uses the new jakarta.* namespaces for the standard library APIs on the Java platform. Because of licensing
and patent issues, the Java community is moving away from the javax.* named packages. But for now, you’ll
have to stick with the javax ones, and versions compatible with them, as the latest version of Spring Security still
uses the javax namespaces. When Spring 6 is released, you can upgrade all these versions, as Spring 5 uses
javax.*, whereas Spring 6 uses the new jakarta.* namespaces.
This new setup is completely disconnected from your existing setup, so you’ll write all this code in a new file.
Instead of adding more stuff to your existing Main.kt, you’ll create a new MainSpringSecurity.kt where all the new
servlet and Jetty-based code will live. Listing 14-1 shows the full main() function you’ll need to get Jetty up and
running, configured in servlet mode.
import org.eclipse.jetty.server.HttpConnectionFactory
import org.eclipse.jetty.server.Server
import org.eclipse.jetty.server.ServerConnector
import org.eclipse.jetty.servlet.ListenerHolder
import org.eclipse.jetty.servlet.ServletContextHandler
fun main() {
val appConfig = createAppConfig(
System.getenv("KOTLINBOOK_ENV") ?: "local"
)
server.handler = ServletContextHandler(
ServletContextHandler.SESSIONS
).apply {
contextPath = "/"
resourceBase = System.getProperty("java.io.tmpdir")
servletContext.setAttribute("appConfig", appConfig)
servletHandler.addListener(
ListenerHolder(BootstrapWebApp::class.java)
)
}
server.start()
server.join()
}
Listing 14-1 Start and configure Jetty for servlet hosting
Jetty is a full-featured Java platform server, which implements lots of APIs, and is extremely flexible. Unlike
embeddedServer in Ktor, Jetty doesn’t have much of any defaults either, so you must explicitly specify
everything to get it up and running.
The main part of this setup is the Jetty server’s handler property, which is set to a
ServletContextHandler. The ServletContextHandler.SESSIONS flag you pass to it enables
servlet sessions, which you’ll need to make Spring Security work. The handler needs a context path, which is the
base path where your web app will be available. It also needs a resource directory to work, which points to the
system default temporary directory so that the operating system automatically cleans up whatever files Jetty
creates. The servlet context, which is available to all individual servlets and filters, gets access to the application
config, which will come in useful later. Finally, a listener is set up, which is a class that the Jetty server executes
when Jetty has finished starting up. This class is where you’ll configure and initialize the actual servlet you’ll use
for your web app and Spring Security itself.
If you type in this code now, you’ll get a compile error for BootstrapWebApp. That class doesn’t exist yet,
so you’ll write that one next.
import javax.servlet.annotation.WebListener
import javax.servlet.ServletContextEvent
import javax.servlet.ServletContextListener
@WebListener
class BootstrapWebApp : ServletContextListener {
override fun contextInitialized(sce: ServletContextEvent) {
val ctx = sce.servletContext
}
}
}
Listing 14-2 A basic shell for servlet initialization
This initializer doesn’t do any work yet. You’ll add more stuff here later in this chapter. For now, try to run
your main class, and check that everything compiles correctly. If you’ve done everything right, your servlet-based
Jetty server should start up fine and listen to the correct port based on your config files. Note that you should stop
your existing main function if it’s already running in the background, as the Jetty server will try to use the same
port since it’s using the same config files and the same config loading code.
Tip The WebListener annotation on BootstrapWebApp is not necessary, and everything will work fine
if you remove it. However, adding it means that you can build and run your web app on an old-school
container-type Java server, and it will pick up the WebListener annotation and mount your servlet, without
running your main() function!
Everything you need to set up will happen in the contextInitialized function of your
BootstrapWebApp class.
You’ll add more things to contextInitialized later. For now, all you need is to extract the config and
get hold of a data source. Listing 14-3 shows how to do this.
log.debug("Extracting config")
val appConfig = ctx.getAttribute(
"appConfig"
) as WebappConfig
The "appConfig" attribute comes from Listing 14-1, where you created the servlet context and assigned the
"appConfig" attribute to your instance of WebappConfig. That means you can access it from the servlet
context, inside contextInitialized. The type of getAttribute is the platform type Object!, so you
need to cast it to a WebappConfig. Then, you create a data source just like you’ve done it in your main Ktor
setup and inside your tests.
BaseApplicationResponse.setupSendPipeline(
appEnginePipeline.sendPipeline
)
appEngineEnvironment.monitor.subscribe(
ApplicationStarting
) {
it.receivePipeline.merge(appEnginePipeline.receivePipeline)
it.sendPipeline.merge(appEnginePipeline.sendPipeline)
it.receivePipeline.installDefaultTransformations()
it.sendPipeline.installDefaultTransformations()
}
ctx.setAttribute(
ServletApplicationEngine
.ApplicationEngineEnvironmentAttributeKey,
appEngineEnvironment
)
Most of this initialization code is based heavily on the existing initialization code in Ktor’s built-in
ServletApplicationEngine, where Ktor tries to create its own instance of a Ktor application. The
appEngineEnvironment and appEnginePipeline are internal objects that the
ServletApplicationEngine needs. There’s also some required wiring for the ApplicationStarting
event, and the ServletApplicationEngine servlet also expects to be able to fetch the
appEngineEnvironment instance on a particular named attribute on the servlet context.
Hopefully, a future release of the servlet package in Ktor makes it easier to pass your own instance of a Ktor
application programmatically. For now, though, you’ll need the preceding wiring code to make everything work.
When you have everything set up, you use the standard servlet API to add a servlet and add a mapping.
You’re now set up with a servlet-based version of your Ktor web app! Try to start the app, and you should see
all your web handlers responding properly, just like when you run your old main function that starts up Ktor using
embeddedServer and Netty.
implementation("org.springframework.security:spring-security-web:5.7.3")
implementation("org.springframework.security:spring-security-config:5.7.3")
Next, you’ll need to wire up Spring Security in your servlet environment. To do that, you need three things: a
role hierarchy, a Spring Security web application context, and a Spring Security filter that’s mapped to your
servlet. Listing 14-5 shows how to set all this up.
AnnotatedBeanDefinitionReader(beanFactory)
.register(WebappSecurityConfig::class.java)
}
}
wac.servletContext = ctx
ctx.addFilter(
"springSecurityFilterChain",
DelegatingFilterProxy("springSecurityFilterChain", wac)
).apply {
addMappingForServletNames(null, false, "ktorServlet")
}
Listing 14-5 Setting up Spring Security inside contextInitialized
You can set up Spring Security without a role hierarchy, but it’s likely that you’re going to need multiple roles,
so you might as well set it up right away.
The web application context is a dependency injection engine where you programmatically register singleton
beans, meaning hard-coded instances of objects that will be dependency injected into your Spring Security config,
for use later, when you configure authentication and filtering in Spring Security.
The "rememberMeKey" is a secret that Spring Security uses to create a session cookie. Instead of hard-
coding to "asdf", you should store this as a config property. By now, you’ve created several config properties, so
you already know how to add more. Go ahead and update your config files, your config data class, and your config
loader code to store the "rememberMeKey" there instead!
import org.springframework.context.annotation.Configuration
import
org.springframework.security.config.annotation.web.configuration.EnableWebSecu
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.context.annotation.Bean
import org.springframework.security.core.userdetails.UserDetailsService
import org.springframework.security.core.userdetails.User
import org.springframework.security.config.annotation.web.builders.HttpSecurit
import org.springframework.security.web.SecurityFilterChain
@Configuration
@EnableWebSecurity
open class WebappSecurityConfig {
@Autowired
lateinit var dataSource: DataSource
@Autowired
lateinit var roleHierarchy: RoleHierarchy
@Autowired
lateinit var rememberMeKey: String
@Autowired
lateinit var userDetailsService: UserDetailsService
@Bean
open fun userDetailsService() =
UserDetailsService { userName ->
User(userName, "{noop}", listOf())
}
@Bean
open fun filterChain(
http: HttpSecurity
): SecurityFilterChain {
return http.build()
}
}
Listing 14-6 An empty Spring Security config class
The annotations @Configuration and @EnableWebSecurity are what cause Spring Security and the
DelegatingFilterProxy from Listing 14-5 to trigger the enabling and configuration of Spring Security in
your servlet chain.
The @Autowired annotation means that Spring Security will assign the object instances (beans) from the
web application context in Listing 14-5 to those properties, based on the property name and its type.
Later, you’ll extend the filterChain to contain the actual configuration of the user and filtering part of
Spring Security.
The UserDetailsService is a required part of the Spring Security setup. For now, all you’ll use it for is
to create a dummy user with a blank password (Spring Security interprets that "{noop}" string to not try to do
anything fancy related to password decoding) with an empty list of roles.
Authenticating Users
To authenticate users, you’ll need to configure the Spring Security filter chain with an authentication provider.
An authentication provider is where Spring Security asks you to handle login credentials, check if that user
exists, and verify that you’ve received the correct credentials (password).
The authentication is set up inside the currently empty filterChain function that you created in Listing 14-
6. Listing 14-7 shows how to set it up.
http.authenticationProvider(object : AuthenticationProvider {
override fun authenticate(
auth: Authentication
): Authentication? {
val username = auth.principal as String
val password = auth.credentials as String
if (userId != null) {
return UsernamePasswordAuthenticationToken(
username,
password,
listOf(SimpleGrantedAuthority("ROLE_USER"))
)
}
return null
}
This code uses the authenticateUser function from Chapter 9 to authenticate users against your existing
setup. Spring Security provides the username and password via the principal and the credentials properties and
expects your authenticate function to return null if you didn’t find a user for those credential, or an
instance of org.springframework.security.core.Authentication if you found a valid user.
For the sake of demonstration, the code also includes a hard-coded example of an admin user, which has the
username "quentin" and the password "test". This is not something you should add to a real-world web app,
for obvious reasons. But it provides an example of what you can do with Spring Security and how to create users
with separate roles.
Filtering Requests
Currently, your Spring Security filter doesn’t filter any requests. To do this, you need to set up a filter chain.
A filter chain is the set of rules that Spring Security uses to determine if a request needs authentication at all
and, if there’s an authenticated user available, if it has the correct access level.
The filter chain is set up using a programmatic builder DSL that is available on the HttpSecurity instance
HTTP inside filterChain of your Spring Security config class. Listing 14-8 shows an example of some of the
things you can do with it.
http
.authorizeRequests()
.expressionHandler(
DefaultWebSecurityExpressionHandler().apply {
setRoleHierarchy(roleHierarchy)
}
)
.antMatchers("/login").permitAll()
.antMatchers("/coroutine_test").permitAll()
.antMatchers("/admin/**").hasRole("ADMIN")
.antMatchers("/**").hasRole("USER")
.anyRequest().authenticated()
.and().formLogin()
.and()
.rememberMe()
.key(rememberMeKey)
.rememberMeServices(
TokenBasedRememberMeServices(
rememberMeKey,
userDetailsService
).apply {
setCookieName("REMEMBER_ME_KOTLINBOOK")
}
)
.and()
.logout()
.logoutRequestMatcher(
AntPathRequestMatcher("/logout")
)
Listing 14-8 Filtering requests using the builder DSL inside of filterChain
There are many ways to set up the filtering, and you need to change the specific config to alter your specific
needs. This serves as an example of some of the possibilities and is a good starting point for most web apps.
The login page is set up to not require authentication, to avoid infinite redirect loops. All pages that start with
/admin require the admin role for that user. Otherwise, the filter requires authentications for all pages. The default
formLogin() is set up as well, which uses the Spring Security default login GUI. It comes with a styled login
form and some default error messages and is a nice way to get started and demonstrate a working setup without
having to implement a login form yourself. Then, logout is set up so that when the user visits the path /logout,
Spring Security logs out the user and clears out the session.
To reiterate, this book is not a comprehensive tutorial on Spring Security. The purpose is to get Spring Security
set up and ready to go, working alongside your library-based web app that you’ve written from scratch.
What’s a Library?
Our industry has no widely agreed-upon definition of the difference
between a library and a framework. The distinction I use is
A library is something you call. A framework is something that calls
you.
Another practical distinction is that in a library, nothing happens that
you didn’t ask for. The trade-off is that you need to wire up things yourself
and write some bootstrapping code to get everything up and running.
In contrast, in a framework, a lot of things happen automatically, to save
you some work and make your life easier. The trade-off is that when
something breaks or if the framework isn’t calling your code automatically
when it should be, it can be difficult to figure out why or even to figure out
that something is wrong in the first place.
One of the main real-world problems I have when I use frameworks is
that when things go wrong, you’re confronted with mountains of framework
code that you weren’t aware of before and that you suddenly must
understand to be able to move forward. Framework code that stops
automatically calling your own code can require lots of work to understand
what happened and why.
There is a gray area here, though. In Chapter 9, you configured Ktor to
handle session cookies and authorization. You don’t see that code
anywhere. All you did was to wrap your routes in
authenticate("auth-session") and hope for the best. There’s a
lot of mapping and action going on under the hood of Ktor to make that
work, and if that stops working, you wouldn’t necessarily know why.
The benefit of Ktor and libraries is that you do call a function
(authenticate) and you refer to the config by name ("auth-
session"). So it’s not that implicit and magical. Ktor won’t do anything
automatically with authentication; you must tell it to.
You’re not necessarily immune from magic that stops working when
you use libraries. But you drastically reduce the chance of that happening,
eliminating a whole category of bugs from your web app.
You’ve learned lots of neat Kotlin tricks along the way, as you’ve
implemented a production-grade web app in the previous chapters. In this
chapter, you’ll learn some more neat Kotlin tricks that I didn’t get to cover.
Many of these tricks are more useful to library authors than web app
builders. In fact, most libraries will likely use most of the concepts
mentioned in this chapter. But it’s still nice to be aware of these concepts.
You might end up debugging library code, and it’s nice to know what
you’re looking at. And you never know! Perhaps you’ll find some use for
the Kotlin tricks in this chapter when you write utilities and helper
functions for your own web apps.
The benefit of delegating with by lazy is that the lambda will only
execute once, the first time you invoke it, whereas the method will execute
the CPU-exhaustive code every time.
You also have control of how Kotlin manages parallel calls from
multiple threads to your lazy property. You can choose between three
thread-safety modes:
NONE: No synchronization between threads. If multiple threads try to
access the property simultaneously while it’s not yet initialized, the
behavior is undefined.
PUBLICATION: Allows multiple computations to run in parallel, but it
uses the result of the first call that successfully completes. Further
invocations after this do not cause the computation to execute.
SYNCHRONIZED: Protects the computation with locks to ensure that it
only runs a single time. Simultaneous calls wait for the first one to
complete.
The default mode is SYNCHRONIZED.
Delegation is a deep subject in Kotlin, and you can do many things with
it, not just lazy initialization. For example, instead of subclassing, you can
delegate a class to an instance of an object, effectively inheriting a class
from an instance. You can read more about it in the official Kotlin
documentation here:
https://kotlinlang.org/docs/delegated-
properties.xhtml.
Inline Functions
You can make functions in Kotlin that have zero overhead by making inline
functions.
Inline functions also have the benefit of not altering the calling context
in any way. A good example of this is the transaction function in
Kotliquery, the SQL library you’ve used in this book. In fact, the pull
request yours truly submitted to Kotliquery, mentioned in Chapter 6
(https://github.com/seratch/kotliquery/pull/57), made
the transaction function inline. Listing 16-1 shows the rough outlines
of the implementation of that function.
This code works fine, but one problem with it is that code inside the
transaction block breaks the coroutine context. Kotlin does not know
statically how a function uses lambda you pass to it. So Kotlin can’t assume
that a function calls a lambda inline with the context it’s declared in. The
following code breaks:
runBlocking {
dbSess.transaction { txSess ->
delay(1)
txSess.run(...)
}
}
The fix for this issue is to mark the function as inline. Kotlin then
knows that the function calls the lambda inline as well. It is possible to
write inline functions that don’t call lambdas inline, but you need to
explicitly mark lambdas with noinline or crossinline if that’s the
case.
By adding the inline keyword (inline fun <A>
Session.transaction(...) to the transaction function in
Kotliquery, the code will run fine, as the Kotlin compiler treats the code in
the lambda passed to transaction just the same it treats code outside of that
lambda.
The built-in scope functions, like apply, with, also, etc. , are inline
functions. That’s why they also work in any context and do not incur any
compile-time or runtime penalties for expressivity or performance.
Reified Generics
Reified generics (or reified type parameters) solve a common pitfall on the
Java platform: generic types are compile-time only. Often referred to as type
erasure, there is no trace left in the compiled bytecode of the generic type.
So, if you have a List<String>, all the bytecode and Java runtime
know is that it’s a List. The type checking for it being strings happens in
compiler passes before it generates the final output.
Kotlin is not able to magically work around Java platform limitations.
But Kotlin does have a solution. The key is inline functions, and you can
only use reified generics in inline functions.
The compiler removes (inlines) inline functions from the compiled
output. The Kotlin compiler copies the body of the function into every
location in your code where you call it. This means that it can replace the
generic type with the actual type that you pass when calling the function. So
a reified generic type to an inline function is just a shortcut for copying that
code manually to all those places you call it from and replacing the generic
with the actual type used at the call site.
For example, let’s say you want to write a helper function that takes a
JSON string and serializes it to the correct type. Listing 16-2 shows how
you could attempt to do that.
This code does not compile, though. That’s because of type erasure.
When this code runs, the type of T is lost. So there’s no way for
serializeJson to know what the type of T is.
However, if you write it as an inline function with a reified generic, the
compiler has everything it knows to make it work. Listing 16-3 shows how
that works.
The code is the same, except for the inline and reified keywords.
Because the compiler inlines the function, it knows what the type of the
generic is, as Kotlin resolves the types of an inline function during
compilation and not in the runtime.
Contracts
Have you ever wondered how Kotlin is able to do this?
If you look carefully at this code, there’s something weird going on. The
type of foo is MyFoo?, which means it can be null. But you’re still able
to call foo.doSomething() without checking that foo is null. Calling a
method on a nullable type is supposed to cause a compile error for null
safety! What is this sorcery?
This is because assertNotNull implements a contract.
This is what assertNotNull looks like without a contract:
This works fine and returns a non-null type. If the value is null, you’ll
get a NullPointerException from the non-null assertion operator
(!!). But that does not solve the mystery. We don’t use the return value
from assertNotNull, so it does not matter to our program that
assertNotNull throws an exception if you pass null to it and return a
non-null type.
The actual implementation of assertNotNull looks like this:
Notice the added contract line. Contracts are ways of telling the type
system about things it cannot infer on its own, but are nevertheless true
statements. Technically speaking, it’s possible to make a contract that
doesn’t hold true in the implementation. But when used correctly, contracts
are powerful additions to the type system that enable third-party libraries,
such as the assertion library in kotlin.test, to extend the built-in type
checks.
The specific contract used here says that if the function returns a value
(i.e., it does not throw an exception), it implies that the argument actual
is non-null. Then, the Kotlin type system can smart cast the type MyFoo?
to MyFoo. You’ve already seen smart casting by checking for null in if
statements, and contracts allows third-party functions to do the same.
Note that contracts are an experimental API. This means that it’s subject
to change in the future, and you must explicitly opt in to use them, using
@OptIn(ExperimentalStdlibApi::class). The opting in is
there so that you don’t accidentally use unstable APIs that might break in
future Kotlin releases and keeps your code future-proof.
Starting a Server
Jooby works like Ktor in that you explicitly start a server at the port you tell it to.
You’ll need to add the following dependencies to your build.gradle.kts:
implementation("io.jooby:jooby:2.16.1")
implementation("io.jooby:jooby-netty:2.16.1")
The next step is to start up a Jooby server. Listing A-1 shows how to wire up the
basics.
fun main() {
val env = System.getenv("KOTLINBOOK_ENV") ?: "local"
val config = createAppConfig(env)
val dataSource = createAndMigrateDataSource(config)
runApp(arrayOf()) {
serverOptions {
port = config.httpPort
server = "netty"
}
coroutine {
get("/") {
"Hello, World!"
}
}
}
}
Listing A-1 Starting a Jooby server
Mapping WebResponse
Your existing handlers are lambdas or functions that return an instance of
WebResponse. You’ve written a mapping layer between WebResponse and Ktor,
and now you’ll need to do the same for WebResponse and Jooby. Listing A-2 shows
how to do that.
fun joobyWebResponse(
handler: suspend HandlerContext.() -> WebResponse
): suspend HandlerContext.() -> Any {
return {
val resp = this.handler()
ctx.setResponseCode(resp.statusCode)
when (resp) {
is TextWebResponse -> {
ctx.responseType = MediaType.text
resp.body
}
is JsonWebResponse -> {
ctx.responseType = MediaType.json
Gson().toJson(resp.body)
}
is HtmlWebResponse -> {
ctx.responseType = MediaType.xhtml
buildString {
appendHTML().xhtml {
with(resp.body) { apply() }
}
}
}
}
}
}
Listing A-2 Mapping WebResponse to Jooby
This code is almost identical to the Ktor mapping code. The difference is in the
type of the lambda that you return and the specific API you use to map to Jooby. For
example, in Ktor, pass in the status code to Ktor’s various response calls, whereas in
Jooby, you call ctx.setResponseCode(). Additionally, Jooby handlers expect
you to return a non-null object of the type Any, which represents the response body of
your route handler.
You’ll also need a variant that prepares a Kotliquery session. Listing A-3 shows
how to implement that.
fun joobyWebResponseDb(
dataSource: DataSource,
handler: suspend HandlerContext.(
dbSess: Session
) -> WebResponse
) = joobyWebResponse {
sessionOf(
dataSource,
returnGeneratedKey = true
).use { dbSess ->
handler(dbSess)
}
}
Listing A-3 Setting up a Kotliquery session for Jooby handlers
This function is also almost identical to the Ktor equivalent and makes use of the
existing joobyWebResponse to wrap the request handler in a Kotliquery session
that it closes when the request completes or if an exception is thrown through the use
scope function.
Serving Assets
Like Ktor, Jooby has a built-in routing setup for dealing with static files.
To set up static file serving, add a mapping for the assets route. Listing A-4
shows how to set it up.
if (config.useFileSystemAssets) {
assets("/*", "src/main/resources/public")
} else {
assets("/*", ClassPathAssetSource(this.classLoader,
"public"))
}
Listing A-4 Serving static assets
When your web app runs locally, you’ll serve assets directly from the file system.
Otherwise, Jooby will load the assets from the class path. Like in the Ktor setup, all
assets in src/main/resources/public are available through the Jooby HTTP server.
Responding to Requests
You’ll respond to request the same way in Jooby as you do when you use Ktor, except
you’ll have to use joobyWebResponse that maps to Jooby, instead of the old
webResponse that maps to Ktor. Listing A-5 shows how to perform some basic
response handling.
get("/", joobyWebResponse {
delay(200)
TextWebResponse("Hello, World!")
})
handleUserEmailSearch(dbSess, input["email"])
})
Listing A-5 Responding to requests inside the coroutine route wrapper in Jooby
Make sure you put this code inside the existing coroutine block inside your
Jooby runApp. If you put it directly under runApp, the call to delay(200) won’t
compile.
This demonstrates how your code is reusable. You wrote
handleUserEmailSearch for Ktor, but because it takes a Kotliquery session and
normal Kotlin data types as input and returns a WebResponse, you can easily invoke
your existing business logic from a completely different routing library than the one
you’ve used it from so far. The main difference is how you parse the request body as
JSON. But handleUserEmailSearch has no knowledge of this.
Authenticating with Forms
Jooby has support for session cookies, so you can add support for HTML-based form
login in a Jooby app. You can also adapt it to work for cookie-based single-page app
authentication, just like in Ktor.
The first step is to set up the session cookie itself. Listing A-6 shows how to
configure Jooby with a session cookie.
sessionStore =
SessionStore.signed(config.cookieSigningKey,
Cookie("joobyCookie")
.setMaxAge(Duration.parse("30d").inWholeSeconds)
.setHttpOnly(true)
.setPath("/")
.setSecure(config.useSecureCookie)
.setSameSite(SameSite.LAX))
Listing A-6 Adding session cookie support to Jooby, inside the runApp block
Unlike Ktor, Jooby only signs the session cookie and does not encrypt it. In
addition to the session data itself, the cookie also includes a signature. This means that
third parties cannot forge values in the cookie, as they do not have access to the signing
secret. But any value you store in the session will be accessible to anyone that has
logged in, by inspecting the cookie contents.
The HTML for the login form is identical to the one in Ktor, but it’s repeated here
in Listing A-7, along with the code for handling the actual login request.
get("/login", joobyWebResponse {
HtmlWebResponse(AppLayout("Log in").apply {
pageBody {
form(method = FormMethod.post, action = "/login") {
p {
label { +"E-mail" }
input(type = InputType.text, name = "username")
}
p {
label { +"Password" }
input(type = InputType.password, name =
"password")
}
post("/login") {
sessionOf(dataSource).use { dbSess ->
val formData = ctx.form()
val userId = authenticateUser(
dbSess,
formData["username"].value(),
formData["password"].value()
)
if (userId == null) {
ctx.sendRedirect("/login")
} else {
ctx.session().put("userId", userId)
ctx.sendRedirect("/secret")
}
}
}
Listing A-7 Displaying a login form and handling the form submission request
Jooby abstracts away the details of the storage and conveniently lets you write to a
key/value-like API. This code stores the ID of the logged-in user in the session cookie,
so that you can retrieve it later.
To protect routes that require login, you’ll wrap them in a decorator. Listing A-
8 demonstrates how to set that up.
path("") {
decorator {
val userId = ctx.session().get("userId").valueOrNull()
if (userId == null) {
ctx.sendRedirect("/login")
} else {
ctx.attribute("userId", userId.toLong())
next.apply(ctx)
}
}
get("/logout") {
ctx.session().destroy()
ctx.sendRedirect("/")
}
}
Listing A-8 Securing routes with a login requirement
The decorator extracts the userId property from the session. Jooby automatically
handles the underlying cookie and provides the same key/value-like API for retrieving
values as for setting them. If the userId is null, you’ll redirect back to the login
page. If not, you’ll call the special next value with the current context, which calls the
actual response handler that Jooby has matched for the current path.
You can use path to wrap routes in a path prefix. In this case, you’ve used path
to wrap a set of routes in a decorator. You could also set this up so that all the routes
that require login get a /secure prefix. To do that, replace path("") with
path("/secure"). Then, the route that was available on /secret will instead be
available on /secure/secret.
You can then use ctx.attribute<Long>("userId")!! to extract the
actual value. You’re using the non-null assertion operator (!!) here, as Jooby will
never invoke the handlers you’ve wrapped in the decorator if the value is null.
implementation("com.sksamuel.hoplite:hoplite-core:2.6.3")
implementation("com.sksamuel.hoplite:hoplite-hocon:2.6.3")
Hoplite has support for many different configuration file formats. You’ll use the
hocon plugin for Hoplite, so that you can reuse the exact same config files for Hoplite
as the existing Typesafe Config–based files.
Next, tell Hoplite to load your config files. Listing B-1 shows how to do that.
The values in app.conf are the base, and any value in app-<env>.conf will override
the default.
The EnvOrSystemPropertyPreprocessor enables inlining of environment
variables in your config files, which you do in app-production.conf. Conveniently, the
syntax in Hoplite is identical to that of Typesafe Config. So your existing config files
will work out of the box.
Finally, loadConfigOrThrow takes a data class as a type parameter, and that’s
the data class that Hoplite will serialize your config properties to.
That’s all there is to it. Run your app, and you have an instance of
WebappConfig available, just like you had before when you used Typesafe Config.
Handling JVM Target Version Errors
Note that by default, IntelliJ generates Kotlin projects with a JVM target version of 1.8.
You can change this default when you generate your projects, but at the time of writing,
you’ll end up with 1.8 if you don’t change anything. Hoplite ships compiled class files
that are compiled using a JVM target version of 11, so if your project still specifies
1.8, you’ll get the following error message at loadConfigOrThrow:
tasks.withType<KotlinCompile> {
kotlinOptions.jvmTarget = "11"
}
When you set the JVM target output version to 11 or later, your code will compile.
Hoplite, however, has error messages that are a bit less informative:
Be aware of this when you run your web app. The error message from Hoplite
makes it look like nothing works and that it can’t find the config file or something like
that. But that’s the error you’ll get if you refer to a nonexisting environment variable in
your config files.
Masking Secrets
Hoplite has built-in support for masking config values, such as database passwords.
This makes it easy to print the value of your config when your web app starts up, where
you want to make sure that you don’t print the actual secret keys and password that
your config provides.
To create a masked value, you’ll update the WebappConfig data class and
replace any String you want to mask with com.sksamuel.hoplite.Masked:
testImplementation("org.spekframework.spek2:spek-dsl-
jvm:2.0.19")
testImplementation("org.spekframework.spek2:spek-runner-
junit5:2.0.19")
testImplementation(kotlin("test"))
package kotlinbook
import org.spekframework.spek2.Spek
import
org.spekframework.spek2.style.specification.describe
import kotlin.test.*
That’s it! You now have a fully functional setup for writing automated tests using
Spek instead of kotlin.test and jUnit 5.
Since you’re using the same assertions library as before, you can copy/paste your
existing kotlin.test-based tests into your new UserSpec specification. The only
thing you must change is that you’ll need to wrap them in it("...") blocks instead
of functions annotated with @Test.
@Test
fun usersVerifiedShouldBeAbleToLogIn() {
// ...
}
@Test
fun usersVerifiedShouldBeAbleToChangePassword() {
// ...
}
@Test
fun usersNotVerifiedShouldNotHaveAccess() {
// ...
}
describe("users") {
describe("verified") {
it("should be able to log in") {
// ...
}
describe("not verified") {
it("should not have access") {
// ...
}
}
}
This lets you group your tests in natural groupings where you have multiple tests
that test the same thing from multiple angles, so that you don’t have to repeat the group
name multiple times.
Skipping Tests
If you’re in the middle of a rewrite or need to temporarily flag a test so it’s skipped,
you can do that with Spek.
Anywhere in your tests where you use describe and it, you can instead write
xdescribe and xit. That marks the test as skipped, and it won’t run the next time
you try to run your tests.
You could just comment out your tests. But the benefit of marking them as skipped
is that the test runner lists all the tests that it detected, but that you’ve flagged for
skipping. That way, you don’t forget about them and accidentally leave them
commented out.
When you’ve installed the plugin, each Spek test gets a little green arrow next to it
to run just that test, exactly like the kotlin.test- and jUnit 5–based tests you’ve
written earlier.
Index
A
Abstractions
Accumulator
also function
API calls
avoid automatic serialization
internal vs. external APIs
normal route
parse input
validating and extracting input
API keys
app.conf
app.css
The <appender>
appEngineEnvironment
AppLayout template
Application plugin
ApplicationCall
ApplicationCall().-> Unit
ApplicationContext class
app-local.conf
app-production.conf
Arrow
assertEquals(2, users.size)
assertNotNull
assertNull
async()
authenticate interceptor
authenticateUser function
Authentication
cookies
JWT
plugin
provider
auth-session
AutoCloseable
Auto-completion
Automatic data serialization
await()
AWS Aurora Serverless
AWS Lambda
AWS API Gateway
cloud providers
console
create function
deploy to AWS Lambda
edit runtime settings
execution result
GetTestHelloWorld
handleCoroutineTest
handleRequest
handleUserEmailSearch
input parameters
JAR file
kotlinbook.GetTestHelloWorld
kotlinbookTestFunction
Ktor handlers
MainServerless.kt
Mapping WebResponse
RequestHandler
RequestHandler interface
serverlessWebResponse function
shadowJar Gradle task
test
Upload from
Azul Zulu
B
Backward-compatible migrations
Bcrypt
bcryptHasher
Beans
Black box tests
Blue/green environment
BootstrapWebApp class
build.gradle.kts
Business logic
ByteArray
ByteBuffer
C
call parameter
call.receiveParameters()
call.receiveText()
call.respondHtml
call.respondHtmlTemplate
call.sessions
Case-insensitive headers
keys
multiple casings
Pair
set-cookie
cause parameter
challenge block
Checked-in config files
class file org/slf4j/impl/StaticLoggerBinder.class
Class hierarchy
Gradle clean task
close() method
componentN() functions
ConfigFactory.load()
config.getBoolean(“httpPort”)
config.getInt(“httpPort”)
config.getInt(“http proot”)
config.httpPort
config.httpProot
Config object
ConfigurationFactory class
Configuration fail early and type-safe
Kotlin null safety
Kotlin platform types
let Blocks
load config, data class
store configuration
web app, data class
Configuration files
defaults in environments
features
reading values
structure
Connection pool
Connection URL
Constructor arguments
Containers
ContentType instance
contextInitialized function
Continuation suspend functions
Contracts
cont.resume(Unit)
Convenience functions, headers
arguments
function overloading
header function
header(String, List<String>)
header(String, String)
header values
List<String>
maps and lists
WebResponse
Cookie-based auth
Cookie-based session storage
cookieEncryptionKey
Cookies
cookieSigningKey
cookieSigningKey (String)
copyResponse
Coroutine internals
comparing with arrow
Java platform implementation
Kotlin vs. KotlinX
low-level continuations
Coroutines
and blocking calls
contexts/functions
delay()
external services
Ktor handlers
parallelizing service calls
suspension
threads problem
web app preparation
coroutineScope
createAndMigrateDataSource()
createAppConfig
createContinuation function
createDataSource()
createKtorApplication()
createStatement()
createUser
Cross-Origin Resource Sharing (CORS)
CSS file
ctx.setResponseCode()
D
Database connections
Database passwords
Database schema
CREATE TABLE statements
creations
Flyway
Flyway migrations
migration library
DataSource
dbSavePoint function
dbSavePoint lambda
dbSess argument
Decoupling web handlers
HTTP response abstraction
Ktor
specific libraries
defaultPage
Defaults override
Deferred<T>
delay() function
DelegatingFilterProxy
Delegation
DELETE statement
Dependencies block
Deployment setup
Destructuring assignment
Different defaults
Dispatchers.IO context
Dockerfile
Docker images
Domain-specific language (DSL)
Double exclamation mark
E
Eclipse IDE
Either instance
Elvis operator
embeddedServer()
Empty implementation
Environment-specific config files
EnvOrSystemPropertyPreprocessor
exception handler blocks
Exception handling
executeQuery()
Execution time
Extension functions
arguments
function types
lambdas
object-oriented programming
receiver type
String
webResponse
F
FactoryBean class
Fail early
Fake service implementation
Fallback configs
filterChain function
Financial transaction data
FlowOrHeadingContent
Flyway
Flyway migration files
flyway_schema_history table
fold function
forEach()
formLogin()
Framework conventions
fromRow
<form> tag
“Full-featured” tests
Full-stack solution
Functional data-driven core
Function header(String, List<String>)
Function reference
Functions returning functions
empty skeleton version
handler lambda
webResponse needs
Function start()
G
getAwsLambdaResponse
getColumnName
getConnection()
GET /login
getObject()
getOrDefault
getRandomBytes(16)
getRandomBytes(32)
GetTestHelloWorld handler function
getUser
GitHub
GraalVM
Gradle
Gradle control panel
Gradle dependencies
Gradle setup
./gradlew application
Gson library
H
h1 function
H2
haltHere
handleCoroutineTest() function
handleCreateOrder function
handleGetFoo function
handler lambda function
Handling failed migrations
failed in production
failed locally
manually performing migration
rerunning
headerName
headers() function
Hard-coded createUser
Header(String, List<String)
header(String, String)
HeaderValue
Helper function
Hexagonal architecture
High-quality logging
error messages
Java platform
production-grade web apps
HikariCP
HikariCP connection pool
HOCON file format
Hoplite
environment variables
JVM target version errors
load config files
masking config values
Typesafe Config
hrefLang
HTML generation
adding custom tags
DSL power
kotlinx.xhtml DSL
Ktor HTML DSL
operator overloading
HtmlWebResponse
abstractions
extension function precedence
implementation
responding with
HTTP API
httpPort
HTTP response abstraction
case-insensitive headers
convenience functions, headers
destructuring assignment
fold function
multiple response types
represent HTTP responses
HTTP responses
HTTP server
HttpStatusCode
HttpStatusCode.fromValue
I
Idiomatic Java
Immutable property
index.xhtml file
Initialization time
Initialization time vs. execution time
Inline functions
INSERT statements
INSERT INTO statements
insert(pageBody)
Instant reloading
Integrated development environment (IDE)
IntelliJ IDEA
auto-completion features
build.gradle.kts
canonical IDE
download and usage
JDK
Kotlin project creation
progress bar
io.ktor.server.application.Application
io.ktor.server.netty.EngineMain.main()
isDevMode
isSingleton()
it.component1()
it.component2()
it.getBoolean(“useFileSystemAssets”)
J
Java archive (JAR) file
Java Development Kit (JDK)
java.io.Closeable
java.lang.AutoCloseable
Java method config.getInt(“...”)
Java method config.getString(“...”)
java.nio.ByteBuffer
Java platform–compatible code
JavaScript
JavaScript framework Next.js
javaSecurityPrincipal
javax.* named packages
JDBC DataSource interface
JetBrains
Jetty
Jooby
authentication
forms
JWTs
Ktor
mapping WebResponse
respond to request
serve assets
start up server
Jooby
joobyWebResponse
JSON
JsonListWebResponse
JsonMapWebResponse
JSON serialization
JSON serializers
JsonWebResponse
JSON Web Tokens (JWTs)
jUnit 5
avoiding leaky tests
industry-standard library
kotlin.test
making test pass
running failing test
TDD
writing failing test
writing web app tests
jvmTarget
K
Koin
Kooby
Kotest
Kotlin
data class
functional programming
language features
metaprogramming
web apps creation
kotlinbook
KOTLINBOOK_ENV
KOTLINBOOK_HTTP_PORT
kotlinbook.Main
Kotlin code
Kotlin community
kotlin.coroutines
kotlin.coroutines APIs
Kotlin data validation libraries
Kotlin Hello, World!
naming conventions
running code with Gradle
running code with IntelliJ IDEA
writing code
Kotlin Native
Kotlin null safety
Kotlin platform types
Kotlin programmers
Kotlin project creation, IntelliJ IDEA
built-in wizard
download
Gradle build system skeleton
GUI
JDK installations
JDK specifications
judgment
LTS releases
New Project dialog
Kotlin-specific dependency injection
kotlin.test
Kotlin tricks
contracts
inline functions
lazy delegation
reified generics
kotlin.UninitializedPropertyAccessException
kotlinx.coroutines
kotlinx.xhtml DSL
kotlinx.serialization
Kotliquery
Kotliquery function sessionOf
Kotliquery session
Ktor
Ktor APIs
Ktor call API
Ktor display
Ktor embedded server
Ktor HTML DSL
Ktor HTTP client
KtorJsonWebResponse
Ktor route mapping
Ktor’s embeddedServer()
ktor-server-servlet
Ktor servlet package
L
Lambdas
Language-native library
lateinit
lateinit var
lateinit var unmigratedDataSource
Lazy delegation
Leaky tests
in-memory database
with relative asserts
testCreateAnotherUser()
testCreateUser
user_t table
with transactions
left() function
let block
Library
boundaries and layers
documentation
Flyway, database migrations
framework
Java
Kotlin
Ktor
language native
language-native library
languages
popularity
stability
trade-off
X way
Library-based web app
<link> tag
Liquibase
Lisp language
list()
listUsers()
load() method
loadConfigOrThrow
Local development environment
logback.xml config file
logback.xml configuration file
Logback XML config file
Logged-in users
LoggerFactory class
Logging
configuration
third-party code
XML config files
Logging config, web app startup
environment variables
formatting output
masking secrets
writing code
Logging implementation
/login
Login form
authenticating users
configuring session cookies
logging in
logging out
new extension function
protecting routes with authentication
session types
Log levels
/logout
Long-running operations
Long-Term Support (LTS)
Low-level Kotlin continuations
M
“Magic” XML config files
main() function
Main.kt file
MainServerlessKt
MainServerlessKt.getDataSource()
Managing schema changes
adding additional schema
adding non-nullable columns
backward-compatible migrations
exceptions
Manually performing migration
mapFromRow() function
Map headers
mapOf()
Mapping JsonWebResponse
Mapping TextWebResponse
Maps vs. data classes
data classes napping
passing around maps
passing individual properties
raw query data
type-safe data classes
metaData object (it)
metaData.columnCount
Metaprogramming
Methodologies
front-end tests
real database writes vs. mocks
testing
unit vs. integration tests
migrateDataSource. getObjectType()
migrateDataSource function
MigratedDataSourceFactoryBean class
Migration fails locally
Migrations and H2
Migration script
Migrations in production
Monadic functional programming
Multiple constructor arguments
Multiple simultaneous transactions
myapp.homeless
myFancyCoroutineFunction()
myTag function
MyThing.myVal
N
Named arguments
Native apps
authentication
cookies
JWT
perform authentication
web-based SPAs
Nested transactions
Non-nullable type
Non-web native apps
NOT NULL
NULL
Nullable type
NullPointerException
O
Official Kotlin documentation
Companion object
Operating system environment
Operating system environment variables
Operator overloading
Oracle DB
OutgoingContent implementation
P
Packaging
as self-contained JAR files
See Self-contained JAR files
production
build Docker images
deploying
run Docker images
pageBody
pageTitle
Pair
Parallelized service calls
adding dependencies
adding to Ktor
handling race conditions
performing
PascalCase
Passing individual properties
password_hash
Password hashing
passwordText
passwordText.toByteArray(Charsets.UTF_8)
Personal identifiable information (PII)
/ping
Placeholder<BODY>
Platform types
plugins block
Plugins
Popularity
Positional vs. named parameters
PostgreSQL
POST /login
Pre-generated server-side HTML
a priori assumption
private val dataSource
“production”
Production-grade Kotlin development
Public-facing API
pushState browser API
Q
Querying from web handlers
avoiding long-running connections
creating helper function
frameworks and architectures
Querying setup
installing Kotliquery
JDBC API
Kotlin
libraries and frameworks
mapping library
mapping results
SQL database
type-safe mapping
Querying SQL databases
execution
maps vs. data classes
set up
transactions
web handlers
queryOperation
R
random()
/random_number
randomNumberRequest
randomNumberRequest.await()
rawConfig
README file
Real-world web apps
registerShutdownHook()
Regulation
Reified generics
Relative asserts
rememberMeKey
Repeatable migrations
respondHtmlLayout
returnGeneratedKey
reverseRequest
right() function
rollback()
The <root> logger
Routing function
Kotliquery Row object
row.anyOrNull(it)
runApp()
runBlocking
S
SameSite
ScheduledThreadPoolExecutor
Schema migration library
SecurityContextHolder
Seed data
arbitrary packages
repeatable migrations
web app
Self-contained JAR files
exection
fat jars
Gradle
run./gradlew shadowJar
shadowJar Gradle task
Shadow plugin install
third-party dependencies
uberjars
sendEmailToUserA
sendEmailToUserB
Serverless environment
AWS Lambda
See AWS Lambda
detached web handlers
Node.js
performance improvement
cold starts
GraalVM
init block
initialization time
initialization time vs. execution time
Java runtime flags
Kotlin/JS
Kotlin Native
lazy loading
MainServerlessKt
migrations and H2
UserEmailSearch handler class
separate web handlers, Ktor
structure
serverlessWebResponse function
Server-side session storage
Serving static files
ServletApplicationEngine
ServletContextHandler
sessionOf
Sessions plugin
Set-Cookie
setSavePoint
setUpKtorCookieSecurity
setUpKtorJwtSecurity
shadowJar Gradle task
Shadow plugin
single()
Single-expression function
singlePageApplication
Single-page apps (SPAs)
authentication users
host alongside API
host separately with CORS
Single test function
SLF4J
/something_else
Sophisticated functional programming
Spek
Kotest
multi-platform
multitude of test definition formats
run individual tests
skipping tests
structure Spek tests
test
Spring Boot
Spring Context
ApplicationContext class
Clojure
create
create data source
extended usage
initialization
lateinit in Kotlin
library-based web app
main() function
set up
Spring Framework
start web app
ThreadPoolExecutor-based data processing
Spring Framework
Spring Security
access logged-in user
authentication user
configuration
filter filter
setup servlet filter
web app
add Ktor to Servlet
initialization Servlet
setup embedded Jetty
.sql suffix
SQL database
database schema
handling failed migrations
seed data
SQL database connection
connection pool
connection pool setup
creation
database driver installing
exceptions
H2 setup
updating WebappConfig
SQL queries execution
additional operations
creating session
inserting rows
multiple rows
positional vs. named parameters
single rows
UPDATE and DELETE statements
static route
statusCode
StatusPages
Storing passwords
Storing secrets, version control
styleLink call
suspend
Suspension
T
Tag interface
Template<HTML>
testDataSource
Test-Driven Development (TDD)
benefits
getUser function
implementation code
tests
writing failing test
testTx
testUserPasswordSalting
testVerifyUserPassword
TextWebResponse
The app.conf configuration file
Third-party dependencies
Third-party JSON serializer
this.pageBody
ThreadPoolExecutor-based data processing
Thread.sleep()
toString() method
Trade-off
Traditional web apps development
adding login form
adding webResponse
CSS and assets
HTML generation
patterns and organization
reusable layouts
users security
Transaction function
TransactionalSession
Transactions
business logic
creation
description
nested transactions
in web handlers
transform function
txSess
txSess.connection.rollback()
Type erasure
Typesafe Config
Type-safe transactional business logic
Type safety
U
Unary plus
underscore_case
unmigratedDataSource
unsafe function
update function
UPDATE statement
updateAndReturnGeneratedKey() function
Updating migration
Updating repeatable migrations
use()
useFileSystemAssets
UserDetailsService
UserEmailSearch handler class
User.fromRow(...)
User interface rendering logic
UserSession data class
Users security
bcrypt
password hashing
UserTests.kt
useSecureCookie (Boolean)
V
val statements
validate block
ValidationError
ValidationError data class
Value semantics
val useFileSystemAssets: Boolean
Verbose implementation
Visual Studio Code
V2 migration
V3 migration
V4 migration
W
WebappConfig
WebappConfig data class
WebappConfig object
WebappConfig(4207)
Web app environment defining
Web application context
WebappSecurityConfig class
Web-based wrappers
Web handlers
environments
HTTP verbs and URL paths
language features
real-world web apps
WebResponse
abstract function
code base
copy function
handler() lambda
headers
header values
instances
Ktor API
Ktor handle
statusCode
subclasses
updation
WebResponse abstraction
WebResponse API
WebResponse business logic
WebResponse data class
WebResponseDb
WebResponse handler
WebResponseTx function
Web Server Hello, World!
adding Ktor library
choosing Ktor
Lambdas
named arguments
running application
separate function extracting
starting web server manually
withContext
withFallback()
Writing web app tests
basics setup
failing test
making test pass
X, Y, Z
XML config file