What You Will Learn
- Setting up Kotlin with Maven
- Creating a Javalin/Kotlin CRUD REST API (no database)
- Some neat Kotlin features (from a Java developer’s perspective)
The instructions for this tutorial will focus on IntelliJ IDEA, as it’s made by JetBrains, the same people who make Kotlin. We recommend downloading the free community edition of IDEA while following this tutorial, but there is also Kotlin support in Eclipse.
Setting up Kotlin with Maven (in IntelliJ IDEA)
The good people over at JetBrains have an up-to-date archetype for Kotlin. To use it, do as follows:
File
->
New
->
Project
Maven
->
Create from archetype
->
org.jetbrains.kotlin:kotlin-archetype-jvm
->
Next
- Follow the instructions and pick a project name
- Create
src/main/kotlin/app/Main.kt
There is no public static void main(String[] args)
in Kotlin,
instead you have a fun main(args: Array<String>)
.
fun main() { // can omit args
println("Hello, world!")
}
Using Javalin with Kotlin
Add the dependency:
- Maven
- Gradle
- SBT
- Grape
- Leiningen
- Buildr
- Ivy
<dependency>
<groupId>io.javalin</groupId>
<artifactId>javalin</artifactId>
<version>6.3.0</version>
</dependency>
Not familiar with Maven? Read our Maven tutorial.
implementation("io.javalin:javalin:6.3.0")
Not familiar with Gradle? Read our Gradle tutorial.
libraryDependencies += "io.javalin" % "javalin" % "6.3.0"
@Grab(group='io.javalin', module='javalin', version='6.3.0')
[io.javalin/javalin "6.3.0"]
'io.javalin:javalin:jar:6.3.0'
<dependency org="io.javalin" name="javalin" rev="6.3.0" />
If you want Javalin with testing tools, Jackson and Logback,
you can use the artifact id javalin-bundle
instead of javalin
.
And paste the “Hello world” example:
import io.javalin.Javalin
fun main() {
val app = Javalin.create().start(7070)
app.get("/") { ctx -> ctx.result("Hello World") }
}
It looks pretty similar to Java8:
Java8: get("/path", ctx -> { ... });
Kotlin: get("/path") { ctx -> ...}
.
The syntax (){}
might look a little strange to Java programmers.
Kotlin supports trailing closures
and provides semicolon inference.
Simplified, this means you don’t have to wrap closures in parentheses and end statements with semicolons.
Creating a Javalin/Kotlin CRUD microservice
Kotlin data-classes
Kotlin has a really neat feature called Data classes. To create a data class you just have to write:
data class User(val name: String, val email: String, val id: Int)
… and you’re done! If you declare all parameters as val
you get an immutable class similar to the
Lombok @Value annotation, only better.
Regardless of if you use var
or val
(or a mix) for your data class,
you get toString, hashCode/equals, copying and destructuring included:
val alice = User(name = "Alice", email = "[email protected]", id = 0)
val aliceNewEmail = alice.copy(email = "[email protected]") // new object with only email changed
val (name, email) = aliceNewEmail // choose the fields you want
println("$name's new email is $email") // prints "Alice's new email is [email protected]"
Initializing some data
Let’s initialize our fake user-database with four users:
val users = mapOf(
0 to User(name = "Alice", email = "[email protected]", id = 0),
1 to User(name = "Bob", email = "[email protected]", id = 1),
2 to User(name = "Carol", email = "[email protected]", id = 2),
3 to User(name = "Dave", email = "[email protected]", id = 3)
)
Kotlin has type inference and named parameters (we could have written our arguments in any order). It also has a nice standard library providing map-literal-like functions.
Creating a data access object
We need to be able to read out data somehow, so let’s set up some basic CRUD functionality, with one added function for finding user by email:
import java.util.concurrent.atomic.AtomicInteger
class UserDao {
// "Initialize" with a few users
// This demonstrates type inference, map-literals and named parameters
val users = hashMapOf(
0 to User(name = "Alice", email = "[email protected]", id = 0),
1 to User(name = "Bob", email = "[email protected]", id = 1),
2 to User(name = "Carol", email = "[email protected]", id = 2),
3 to User(name = "Dave", email = "[email protected]", id = 3)
)
var lastId: AtomicInteger = AtomicInteger(users.size - 1)
fun save(name: String, email: String) {
val id = lastId.incrementAndGet()
users[id] = User(name = name, email = email, id = id)
}
fun findById(id: Int): User? {
return users[id]
}
fun findByEmail(email: String): User? {
return users.values.find { it.email == email } // == is equivalent to java .equals() (referential equality is checked by ===)
}
fun update(id: Int, user: User) {
users[id] = User(name = user.name, email = user.email, id = id)
}
fun delete(id: Int) {
users.remove(id)
}
}
The findByEmail
function shows of some neat features. In addition to the
trailing closures that we saw earlier, Kotlin also has a very practical find
function
and a special it
keyword, which replaces user -> user
style declarations with just it
(docs).
The function also demonstrates that ==
is the structural equality operator for Strings in Kotlin
(equivalent to .equals()
in Java). If you want to check for referential equality in Kotlin you can use ===
.
Another thing worth noticing is that the find-functions return User?
, which means the function will
return either a User
or null
. In Kotlin you have to specify the possibility of a null-return.
findByEmail()
, Kotlin vs Java:
// Kotlin
fun findByEmail(email: String): User? {
return users.values.find { it.email == email }
}
// Java
public User findByEmail(String email) {
return users.values().stream()
.filter(user -> user.getEmail().equals(email))
.findFirst()
.orElse(null);
}
Creating the REST API
Kotlin and Javalin play very well together (in fact, Kotlin seems to play well with all Java dependencies).
import app.user.User
import app.user.UserDao
import io.javalin.apibuilder.ApiBuilder.*
import io.javalin.Javalin
import io.javalin.http.HttpStatus
import io.javalin.http.NotFoundResponse
import io.javalin.http.bodyAsClass
import io.javalin.http.pathParamAsClass
fun main() {
val userDao = UserDao()
val app = Javalin.create {
it.router.apiBuilder {
get("/") { it.redirect("/users") } // redirect root to /users
get("/users") { ctx ->
ctx.json(userDao.users)
}
get("/users/{user-id}") { ctx ->
val userId = ctx.pathParamAsClass<Int>("user-id").get()
val user = userDao.findById(userId) ?: throw NotFoundResponse()
ctx.json(user)
}
get("/users/email/{email}") { ctx ->
val email = ctx.pathParam("email")
val user = userDao.findByEmail(email) ?: throw NotFoundResponse()
ctx.json(user)
}
post("/users") { ctx ->
val user = ctx.bodyAsClass<User>()
userDao.save(name = user.name, email = user.email)
ctx.status(201)
}
patch("/users/{user-id}") { ctx ->
val userId = ctx.pathParamAsClass<Int>("user-id").get()
val user = ctx.bodyAsClass<User>()
userDao.update(id = userId, user = user)
ctx.status(204)
}
delete("/users/{user-id}") { ctx ->
val userId = ctx.pathParamAsClass<Int>("user-id").get()
userDao.delete(userId)
ctx.status(204)
}
}
}.apply {
exception(Exception::class.java) { e, ctx -> e.printStackTrace() }
error(HttpStatus.NOT_FOUND) { ctx -> ctx.json("not found") }
}.start(7070)
}
Conclusion
I had only worked with Kotlin for a few hours before writing this tutorial, but I’m already a very big fan of the language. Everything just seems to make sense, and the interoperability with Java is great. IntelliJ will also automatically convert Java code into Kotlin if you paste it into your project. Please clone the repo and give it a try!