Skip to content

Building web applications with Spring Boot and Kotlin :: Learn how to easily build and test web applications with Spring, Kotlin, Junit and JDBC

Notifications You must be signed in to change notification settings

spring-guides/tut-spring-boot-kotlin

This tutorial shows you how to efficiently build a sample blog application by combining the power of Spring Boot and Kotlin.

If you are starting with Kotlin, you can learn the language by taking the Kotlin tour or using the Spring Framework reference documentation, which provides code samples in Kotlin.

Spring Kotlin support is documented in the Spring Framework and Spring Boot reference documentation. If you need help, search or ask questions with the spring and kotlin tags on StackOverflow or come discuss in the #spring channel of Kotlin Slack.

Creating a New Project

First, we need to create a Spring Boot application, which can be done in a number of ways.

Using the Initializr Website

Visit https://start.spring.io and choose the Kotlin language. Gradle is the most commonly used build tool in Kotlin, and it provides a Kotlin DSL which is used by default when generating a Kotlin project, so this is the recommended choice. But you can also use Maven if you are more comfortable with it. Notice that you can use https://start.spring.io/#!language=kotlin&type=gradle-project-kotlin to have Kotlin and Gradle selected by default.

  1. Select "Gradle - Kotlin" or "Maven" depending on which build tool you want to use

  2. Enter the following artifact coordinates: blog

  3. Add the following dependencies:

    • Spring Web

    • Mustache

    • Spring Data JDBC

    • H2 Database

    • Spring Boot DevTools

  4. Click "Generate Project".

The .zip file contains a standard project in the root directory, so you might want to create an empty directory before you unpack it.

Using command line

You can use the Initializr HTTP API from the command line with, for example, curl on a UN*X like system:

$ mkdir blog && cd blog
$ curl https://start.spring.io/starter.zip -d language=kotlin -d type=gradle-project-kotlin -d dependencies=web,mustache,jdbc,h2,devtools -d packageName=com.example.blog -d name=Blog -o blog.zip

Add -d type=gradle-project if you want to use Gradle.

Using IntelliJ IDEA

Spring Initializr is also integrated in IntelliJ IDEA Ultimate edition and allows you to create and import a new project without having to leave the IDE for the command-line or the web UI.

To access the wizard, go to File | New | Project, and select Spring Initializr.

Follow the steps of the wizard to use the following parameters:

  • Artifact: "blog"

  • Type: "Gradle - Kotlin" or "Maven"

  • Language: Kotlin

  • Name: "Blog"

  • Dependencies: "Spring Web Starter", "Mustache", "Spring Data JDBC", "H2 Database", and "Spring Boot DevTools"

Null safety

One of Kotlin’s key features is null safety, which cleanly deals with null values at compile time rather than bumping into the famous NullPointerException at runtime. This makes applications safer through nullability declarations and expressing "value or no value" semantics without paying the cost of wrappers like Optional. Note that Kotlin allows using functional constructs with nullable values; check out this comprehensive guide to Kotlin null-safety.

Although Java does not allow one to express null-safety in its type-system, Spring provides null safety of its APIs via tooling-friendly JSpecify annotations. Kotlin translates JSpecify annotations on Java APIs to Kotlin nullability out-of-the-box.

Understanding the Build

Plugins

In addition to the obvious Kotlin Gradle or Maven plugin, the default configuration declares the kotlin-spring plugin which automatically opens classes and methods (unlike in Java, the default qualifier is final in Kotlin) annotated or meta-annotated with Spring annotations. This is useful to be able to create @Configuration or @Transactional beans without having to add the open qualifier required by CGLIB proxies for example.

build.gradle.kts
plugins {
    id("org.springframework.boot") version "4.0.0"
    id("io.spring.dependency-management") version "1.1.7"
    kotlin("jvm") version "2.2.21"
    kotlin("plugin.spring") version "2.2.21"
}

// ...

kotlin {
    compilerOptions {
        freeCompilerArgs.addAll("-Xjsr305=strict", "-Xannotation-default-target=param-property")
    }
}
pom.xml
<build>
    <sourceDirectory>${project.basedir}/src/main/kotlin</sourceDirectory>
    <testSourceDirectory>${project.basedir}/src/test/kotlin</testSourceDirectory>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
        </plugin>
    <plugin>
        <artifactId>kotlin-maven-plugin</artifactId>
        <groupId>org.jetbrains.kotlin</groupId>
        <configuration>
            <args>
                <arg>-Xjsr305=strict</arg>
                <arg>-Xannotation-default-target=param-property</arg>
            </args>
            <compilerPlugins>
                <plugin>spring</plugin>
            </compilerPlugins>
        </configuration>
        <dependencies>
            <dependency>
                <groupId>org.jetbrains.kotlin</groupId>
                <artifactId>kotlin-maven-allopen</artifactId>
                <version>${kotlin.version}</version>
            </dependency>
        </dependencies>
    </plugin>
    </plugins>
</build>

Dependencies

Kotlin-specific libraries are required for such a Spring Boot web application and configured by default:

  • kotlin-stdlib is the Kotlin standard library (added automatically with Gradle)

  • kotlin-reflect is the Kotlin reflection library

  • jackson-module-kotlin adds support for serialization/deserialization of Kotlin classes and data classes (single constructor classes can be used automatically, and those with secondary constructors or static factories are also supported)

build.gradle.kts
dependencies {
    implementation("org.springframework.boot:spring-boot-starter-data-jdbc")
    implementation("org.springframework.boot:spring-boot-starter-mustache")
    implementation("org.springframework.boot:spring-boot-starter-webmvc")
    implementation("tools.jackson.module:jackson-module-kotlin")
    implementation("org.jetbrains.kotlin:kotlin-reflect")
    runtimeOnly("com.h2database:h2")
    runtimeOnly("org.springframework.boot:spring-boot-devtools")
    testImplementation("org.springframework.boot:spring-boot-starter-data-jdbc-test")
    testImplementation("org.springframework.boot:spring-boot-starter-webmvc-test")
    testImplementation("org.jetbrains.kotlin:kotlin-test-junit5")
}
pom.xml
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jdbc</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-mustache</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-webmvc</artifactId>
    </dependency>
    <dependency>
        <groupId>tools.jackson.module</groupId>
        <artifactId>jackson-module-kotlin</artifactId>
    </dependency>
    <dependency>
        <groupId>org.jetbrains.kotlin</groupId>
        <artifactId>kotlin-reflect</artifactId>
    </dependency>
    <dependency>
        <groupId>org.jetbrains.kotlin</groupId>
        <artifactId>kotlin-stdlib</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-devtools</artifactId>
        <scope>runtime</scope>
    </dependency>
    <dependency>
        <groupId>com.h2database</groupId>
        <artifactId>h2</artifactId>
        <scope>runtime</scope>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jdbc-test</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-webmvc-test</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.jetbrains.kotlin</groupId>
        <artifactId>kotlin-test-junit5</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

Database Schema

We need to create a database schema file since Spring Data JDBC doesn’t auto-create tables.

src/main/resources/schema.sql

CREATE TABLE IF NOT EXISTS "users" (
    "ID" BIGINT AUTO_INCREMENT PRIMARY KEY,
    "LOGIN" VARCHAR(255) NOT NULL UNIQUE,
    "FIRSTNAME" VARCHAR(255) NOT NULL,
    "LASTNAME" VARCHAR(255) NOT NULL,
    "DESCRIPTION" TEXT
);

CREATE TABLE IF NOT EXISTS "article" (
    "ID" BIGINT AUTO_INCREMENT PRIMARY KEY,
    "TITLE" VARCHAR(255) NOT NULL,
    "HEADLINE" VARCHAR(500) NOT NULL,
    "CONTENT" TEXT NOT NULL,
    "author_id" BIGINT NOT NULL,
    "SLUG" VARCHAR(255) NOT NULL UNIQUE,
    "ADDED_AT" TIMESTAMP NOT NULL,
    FOREIGN KEY ("author_id") REFERENCES "users"("ID")
);

Understanding the generated Application

src/main/kotlin/com/example/blog/BlogApplication.kt

package com.example.blog

import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication

@SpringBootApplication
class BlogApplication

fun main(args: Array<String>) {
    runApplication<BlogApplication>(*args)
}

Compared to Java, you can notice the lack of semicolons, the lack of brackets on empty class (you can add some if you need to declare beans via @Bean annotation) and the use of runApplication top level function. runApplication<BlogApplication>(*args) is Kotlin idiomatic alternative to SpringApplication.run(BlogApplication::class.java, *args) and can be used to customize the application with the following syntax:

src/main/kotlin/com/example/blog/BlogApplication.kt

fun main(args: Array<String>) {
    runApplication<BlogApplication>(*args) {
        setBannerMode(Banner.Mode.OFF)
    }
}

Writing your first Kotlin controller

Let’s create a simple controller to display a simple web page.

src/main/kotlin/com/example/blog/HtmlController.kt

package com.example.blog

import org.springframework.stereotype.Controller
import org.springframework.ui.Model
import org.springframework.ui.set
import org.springframework.web.bind.annotation.GetMapping

@Controller
class HtmlController {

    @GetMapping("/")
    fun blog(model: Model): String {
        model["title"] = "Blog"
        return "blog"
    }
}

Notice that we are using here a Kotlin extension that allows adding Kotlin functions or operators to existing Spring types. Here we import the org.springframework.ui.set extension function in order to be able to write model["title"] = "Blog" instead of model.addAttribute("title", "Blog"). The Spring Framework KDoc API lists all the Kotlin extensions provided to enrich the Java API.

We also need to create the associated Mustache templates.

src/main/resources/templates/header.mustache

<html>
<head>
    <title>{{title}}</title>
</head>
<body>

src/main/resources/templates/footer.mustache

</body>
</html>

src/main/resources/templates/blog.mustache

{{> header}}

<h1>{{title}}</h1>

{{> footer}}

Start the web application by running the main function of BlogApplication.kt, and go to http://localhost:8080/, you should see a sober web page with a "Blog" headline.

Testing with JUnit

JUnit, used by default in Spring Boot, provides various features very handy with Kotlin, including autowiring of constructor/method parameters which allows using non-nullable val properties and the possibility to use @BeforeAll/@AfterAll on regular non-static methods.

Writing JUnit tests in Kotlin

For the sake of this example, let’s create an integration test to demonstrate various features:

  • We use real sentences between backticks instead of a camel-case to provide expressive test function names

  • JUnit allows injecting constructor and method parameters, which is a good fit with Kotlin read-only and non-nullable properties

  • This code leverages the expectBody Kotlin extension (you need to import it)

src/test/kotlin/com/example/blog/IntegrationTests.kt

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureRestTestClient
class IntegrationTests(@Autowired val restClient: RestTestClient) {

    @Test
    fun `Assert blog page title, content and status code`() {
        println(">> Assert blog page title, content and status code")
        restClient.get().uri("/")
            .exchangeSuccessfully()
            .expectBody<String>()
            .value { assertThat(it).contains("<h1>Blog</h1>", "Lorem") }
    }
}

Test instance lifecycle

Sometimes you need to execute a method before or after all tests of a given class. JUnit requires by default these methods to be static (which translates to companion object in Kotlin, which is quite verbose and not straightforward) because test classes are instantiated one time per test.

But Junit allows you to change this default behavior and instantiate test classes one time per class. This can be done in various ways, here we will use a property file to change the default behavior for the whole project:

src/test/resources/junit-platform.properties

junit.jupiter.testinstance.lifecycle.default = per_class

With this configuration, we can now use the @BeforeAll and @AfterAll annotations on regular methods like shown in the updated version of IntegrationTests above.

src/test/kotlin/com/example/blog/IntegrationTests.kt

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureRestTestClient
class IntegrationTests(@Autowired val restClient: RestTestClient) {

    @BeforeAll
    fun setup() {
        println(">> Setup")
    }

    @Test
    fun `Assert blog page title, content and status code`() {
        println(">> Assert blog page title, content and status code")
        restClient.get().uri("/")
            .exchangeSuccessfully()
            .expectBody<String>()
            .value { assertThat(it).contains("<h1>Blog</h1>", "Lorem") }
    }

    @Test
    fun `Assert article page title, content and status code`() {
        println(">> TODO")
    }

    @AfterAll
    fun teardown() {
        println(">> Tear down")
    }
}

Creating your own extensions

Instead of using util classes with abstract methods like in Java, it is usual in Kotlin to provide such functionalities via Kotlin extensions. Here we are going to add a format() function to the existing LocalDateTime type in order to generate text with the English date format.

src/main/kotlin/com/example/blog/Extensions.kt

fun LocalDateTime.format(): String = this.format(englishDateFormatter)

private val daysLookup = (1..31).associate { it.toLong() to getOrdinal(it) }

private val englishDateFormatter = DateTimeFormatterBuilder()
    .appendPattern("yyyy-MM-dd")
    .appendLiteral(" ")
    .appendText(ChronoField.DAY_OF_MONTH, daysLookup)
    .appendLiteral(" ")
    .appendPattern("yyyy")
    .toFormatter(Locale.ENGLISH)

private fun getOrdinal(n: Int) = when {
    n in 11..13 -> "${n}th"
    n % 10 == 1 -> "${n}st"
    n % 10 == 2 -> "${n}nd"
    n % 10 == 3 -> "${n}rd"
    else -> "${n}th"
}

fun String.toSlug() = lowercase(Locale.getDefault())
    .replace("\n", " ")
    .replace("[^a-z\\d\\s]".toRegex(), " ")
    .split(" ")
    .joinToString("-")
    .replace("-+".toRegex(), "-")

We will leverage these extensions in the next section.

Fetching with Spring Data JDBC

To fetch data, with Spring Data JDBC, we use immutable data classes, which is a more idiomatic approach in Kotlin.

We create our model by using Kotlin primary constructor concise syntax and data classes which allows declaring at the same time the properties and the constructor parameters.

src/main/kotlin/com/example/blog/Entities.kt

@Table("article")
data class Article(
    val title: String,
    val headline: String,
    val content: String,
    @Column("author_id")
    val author: AggregateReference<User, Long>,
    val slug: String = title.toSlug(),
    val addedAt: LocalDateTime = LocalDateTime.now(),
    @Id val id: Long? = null
)

@Table("users")
data class User(
    val login: String,
    val firstname: String,
    val lastname: String,
    val description: String? = null,
    @Id val id: Long? = null
)

Notice that we are using here our String.toSlug() extension to provide a default argument to the slug parameter of Article constructor. Optional parameters with default values are defined at the last position to make it possible to omit them when using positional arguments (Kotlin also supports named arguments). Notice that in Kotlin it is not unusual to group concise class declarations in the same file.

The @Table annotation maps the classes to database tables, and @Column is used for the foreign key relationship. Spring Data JDBC uses AggregateReference to represent foreign key relationships, which only stores the ID rather than loading the entire related entity.

We also declare our Spring Data JDBC repositories as follows:

src/main/kotlin/com/example/blog/Repositories.kt

interface ArticleRepository : CrudRepository<Article, Long> {
    fun findBySlug(slug: String): Article?
    fun findAllByOrderByAddedAtDesc(): Iterable<Article>
}

interface UserRepository : CrudRepository<User, Long> {
    fun findByLogin(login: String): User?
}

And we write JDBC tests to check whether basic use cases work as expected.

src/test/kotlin/com/example/blog/RepositoriesTests.kt

@DataJdbcTest
class RepositoriesTests @Autowired constructor(
    val userRepository: UserRepository,
    val articleRepository: ArticleRepository
) {

    @Test
    fun `When findByIdOrNull then return Article`() {
        val johnDoe = userRepository.save(User("johnDoe", "John", "Doe"))
        val article = articleRepository.save(
            Article("Lorem", "Lorem", "dolor sit amet", AggregateReference.to(johnDoe.id!!))
        )
        val found = articleRepository.findByIdOrNull(article.id!!)
        assertThat(found).isEqualTo(article)
    }

    @Test
    fun `When findByLogin then return User`() {
        val johnDoe = userRepository.save(User("johnDoe", "John", "Doe"))
        val user = userRepository.findByLogin(johnDoe.login)
        assertThat(user).isEqualTo(johnDoe)
    }
}
Note
We use here the CrudRepository.findByIdOrNull Kotlin extension provided by default with Spring Data, which is a nullable variant of the Optional based CrudRepository.findById. Read the great Null is your friend, not a mistake blog post for more details.

Implementing the blog engine

We update the "blog" Mustache templates.

src/main/resources/templates/blog.mustache

{{> header}}

<h1>{{title}}</h1>

<div class="articles">

    {{#articles}}
        <section>
            <header class="article-header">
                <h2 class="article-title"><a href="/article/{{slug}}">{{title}}</a></h2>
                <div class="article-meta">By  <strong>{{author.firstname}}</strong>, on <strong>{{addedAt}}</strong></div>
            </header>
            <div class="article-description">
                {{headline}}
            </div>
        </section>
    {{/articles}}
</div>

{{> footer}}

And we create an "article" new one.

src/main/resources/templates/article.mustache

{{> header}}

<section class="article">
    <header class="article-header">
        <h1 class="article-title">{{article.title}}</h1>
        <p class="article-meta">By  <strong>{{article.author.firstname}}</strong>, on <strong>{{article.addedAt}}</strong></p>
    </header>

    <div class="article-description">
        {{article.headline}}

        {{article.content}}
    </div>
</section>

{{> footer}}

We update the HtmlController in order to render blog and article pages with the formatted date. Both ArticleRepository and UserRepository constructor parameters will be automatically autowired since HtmlController has a single constructor (implicit @Autowired). Notice that with Spring Data JDBC, we need to manually resolve the author relationship by looking up the user via their ID.

src/main/kotlin/com/example/blog/HtmlController.kt

@Controller
class HtmlController(
    private val articleRepository: ArticleRepository,
    private val userRepository: UserRepository,
    private val properties: BlogProperties
) {

    @GetMapping("/")
    fun blog(model: Model): String {
        model["title"] = properties.title
        model["banner"] = properties.banner
        model["articles"] = articleRepository.findAllByOrderByAddedAtDesc()
            .map { it.render() }
        return "blog"
    }

    @GetMapping("/article/{slug}")
    fun article(@PathVariable slug: String, model: Model): String {
        val article = articleRepository.findBySlug(slug)
            ?: throw ResponseStatusException(NOT_FOUND, "This article does not exist")

        val renderedArticle = article.render()
        model["title"] = renderedArticle.title
        model["article"] = renderedArticle
        return "article"
    }

    private fun Article.render(): RenderedArticle {
        val author = userRepository.findById(author.id)
            .orElseThrow { ResponseStatusException(NOT_FOUND, "Author not found") }

        return RenderedArticle(
            slug,
            title,
            headline,
            content,
            author,
            addedAt.format()
        )
    }

    data class RenderedArticle(
        val slug: String,
        val title: String,
        val headline: String,
        val content: String,
        val author: User,
        val addedAt: String
    )
}

Then, we add data initialization to a new BlogConfiguration class. Notice that with Spring Data JDBC, we use AggregateReference.to() to create foreign key references.

src/main/kotlin/com/example/blog/BlogConfiguration.kt

@Configuration
class BlogConfiguration {

    @Bean
    fun databaseInitializer(
        userRepository: UserRepository,
        articleRepository: ArticleRepository
    ) = ApplicationRunner {
        val johnDoe = userRepository.save(User("johnDoe", "John", "Doe"))
        articleRepository.save(
            Article(
                title = "Lorem",
                headline = "Lorem",
                content = "dolor sit amet",
                author = AggregateReference.to(johnDoe.id!!)
            )
        )

        articleRepository.save(
            Article(
                title = "Ipsum",
                headline = "Ipsum",
                content = "dolor sit amet",
                author = AggregateReference.to(johnDoe.id)
            )
        )
    }
}
Note
Notice the usage of named parameters to make the code more readable. The AggregateReference.to() method creates a reference to the related entity using only its ID.

And we also update the integration tests accordingly.

src/test/kotlin/com/example/blog/IntegrationTests.kt

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureRestTestClient
class IntegrationTests(@Autowired val restClient: RestTestClient) {

    @BeforeAll
    fun setup() {
        println(">> Setup")
    }

    @Test
    fun `Assert blog page title, content and status code`() {
        println(">> Assert blog page title, content and status code")
        restClient.get().uri("/")
            .exchangeSuccessfully()
            .expectBody<String>()
            .value { assertThat(it).contains("<h1>Blog</h1>", "Lorem") }
    }

    @Test
    fun `Assert article page title, content and status code`() {
        println(">> Assert article page title, content and status code")
        val title = "Lorem"
        restClient.get().uri("/article/${title.toSlug()}")
            .exchangeSuccessfully()
            .expectBody<String>()
            .value { assertThat(it).contains(title, "Lorem", "dolor sit amet") }
    }

    @AfterAll
    fun teardown() {
        println(">> Tear down")
    }
}

Start (or restart) the web application, and go to http://localhost:8080/, you should see the list of articles with clickable links to see a specific article.

Exposing HTTP API

We are now going to implement the HTTP API via @RestController annotated controllers.

src/main/kotlin/com/example/blog/HttpControllers.kt

@RestController
@RequestMapping("/api/article")
class ArticleController(
    private val articleRepository: ArticleRepository,
    private val userRepository: UserRepository
) {

    @GetMapping("/")
    fun findAll() = articleRepository.findAllByOrderByAddedAtDesc().map { it.toDto() }

    @GetMapping("/{slug}")
    fun findOne(@PathVariable slug: String) =
        articleRepository.findBySlug(slug)?.toDto()
            ?: throw ResponseStatusException(NOT_FOUND, "This article does not exist")

    private fun Article.toDto(): ArticleDto {
        val author = userRepository.findById(author.id)
            .orElseThrow { ResponseStatusException(NOT_FOUND, "Author not found") }
        return ArticleDto(slug, title, headline, content, author, addedAt.format())
    }

    data class ArticleDto(
        val slug: String,
        val title: String,
        val headline: String,
        val content: String,
        val author: User,
        val addedAt: String
    )
}

@RestController
@RequestMapping("/api/user")
class UserController(private val repository: UserRepository) {

    @GetMapping("/")
    fun findAll() = repository.findAll()

    @GetMapping("/{login}")
    fun findOne(@PathVariable login: String) =
        repository.findByLogin(login)
            ?: throw ResponseStatusException(NOT_FOUND, "This user does not exist")
}

For tests, instead of integration tests, we are going to leverage @WebMvcTest and Mockk which is similar to Mockito but better suited for Kotlin.

Since @MockBean and @SpyBean annotations are specific to Mockito, we are going to leverage SpringMockK which provides similar @MockkBean and @SpykBean annotations for Mockk.

build.gradle.kts
testImplementation("com.ninja-squad:springmockk:5.0.1")
pom.xml
<dependency>
    <groupId>com.ninja-squad</groupId>
    <artifactId>springmockk</artifactId>
    <version>5.0.1</version>
    <scope>test</scope>
</dependency>

src/test/kotlin/com/example/blog/HttpControllersTests.kt

@WebMvcTest
class HttpControllersTests(@Autowired val mockMvc: MockMvc) {

    @MockkBean
    lateinit var userRepository: UserRepository

    @MockkBean
    lateinit var articleRepository: ArticleRepository

    @Test
    fun `List articles`() {
        val johnDoe = User("johnDoe", "John", "Doe", id = 1L)
        val authorRef = AggregateReference.to<User, Long>(1L)
        val lorem5Article = Article("Lorem", "Lorem", "dolor sit amet", authorRef)
        val ipsumArticle = Article("Ipsum", "Ipsum", "dolor sit amet", authorRef)
        every { userRepository.findById(1L) } returns Optional.of(johnDoe)
        every { articleRepository.findAllByOrderByAddedAtDesc() } returns listOf(lorem5Article, ipsumArticle)
        mockMvc.perform(get("/api/article/").accept(MediaType.APPLICATION_JSON))
            .andExpect(status().isOk)
            .andExpect(content().contentType(MediaType.APPLICATION_JSON))
            .andExpect(jsonPath("\$.[0].author.login").value(johnDoe.login))
            .andExpect(jsonPath("\$.[0].slug").value(lorem5Article.slug))
            .andExpect(jsonPath("\$.[1].author.login").value(johnDoe.login))
            .andExpect(jsonPath("\$.[1].slug").value(ipsumArticle.slug))
    }

    @Test
    fun `List users`() {
        val johnDoe = User("johnDoe", "John", "Doe")
        val janeDoe = User("janeDoe", "Jane", "Doe")
        every { userRepository.findAll() } returns listOf(johnDoe, janeDoe)
        mockMvc.perform(get("/api/user/").accept(MediaType.APPLICATION_JSON))
            .andExpect(status().isOk)
            .andExpect(content().contentType(MediaType.APPLICATION_JSON))
            .andExpect(jsonPath("\$.[0].login").value(johnDoe.login))
            .andExpect(jsonPath("\$.[1].login").value(janeDoe.login))
    }
}
Note
$ needs to be escaped in strings as it is used for string interpolation.

Configuration properties

In Kotlin, the recommended way to manage your application properties is to use read-only properties.

src/main/kotlin/com/example/blog/BlogProperties.kt

@ConfigurationProperties("blog")
data class BlogProperties(var title: String, val banner: Banner) {
    data class Banner(val title: String? = null, val content: String)
}

Then we enable it at BlogApplication level.

src/main/kotlin/com/example/blog/BlogApplication.kt

@SpringBootApplication
@EnableConfigurationProperties(BlogProperties::class)
class BlogApplication

Your custom properties should now be recognized when editing application.properties (autocomplete, validation, etc.).

src/main/resources/application.properties

blog.title=Blog
blog.banner.title=Warning
blog.banner.content=The blog will be down tomorrow.
spring.sql.init.mode=always

Edit the template and the controller accordingly.

src/main/resources/templates/blog.mustache

{{> header}}

<div class="articles">

    {{#banner.title}}
    <section>
        <header class="banner">
            <h2 class="banner-title">{{banner.title}}</h2>
        </header>
        <div class="banner-content">
            {{banner.content}}
        </div>
    </section>
    {{/banner.title}}

    ...

</div>

{{> footer}}

src/main/kotlin/com/example/blog/HtmlController.kt

@Controller
class HtmlController(
    private val articleRepository: ArticleRepository,
    private val userRepository: UserRepository,
    private val properties: BlogProperties
) {

    @GetMapping("/")
    fun blog(model: Model): String {
        model["title"] = properties.title
        model["banner"] = properties.banner
        model["articles"] = articleRepository.findAllByOrderByAddedAtDesc()
            .map { it.render() }
        return "blog"
    }
    // ...

Restart the web application, refresh http://localhost:8080/, you should see the banner on the blog homepage.

Conclusion

We have now finished building this sample Kotlin blog application. The source code is available on GitHub. You can also have a look at Spring Framework and Spring Boot reference documentation if you need more details on specific features.

About

Building web applications with Spring Boot and Kotlin :: Learn how to easily build and test web applications with Spring, Kotlin, Junit and JDBC

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published