{"id":14977513,"url":"https://github.com/spring-guides/tut-spring-boot-kotlin","last_synced_at":"2025-05-14T18:02:37.588Z","repository":{"id":40542339,"uuid":"131296014","full_name":"spring-guides/tut-spring-boot-kotlin","owner":"spring-guides","description":"Building web applications with Spring Boot and Kotlin :: Learn how to easily build and test web applications with Spring, Kotlin, Junit 5 and JPA","archived":false,"fork":false,"pushed_at":"2025-02-24T21:42:37.000Z","size":820,"stargazers_count":939,"open_issues_count":0,"forks_count":293,"subscribers_count":41,"default_branch":"main","last_synced_at":"2025-05-14T18:02:18.328Z","etag":null,"topics":["spring-boot","spring-framework"],"latest_commit_sha":null,"homepage":"https://spring.io/guides/tutorials/spring-boot-kotlin/","language":"Kotlin","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/spring-guides.png","metadata":{"files":{"readme":"README.adoc","changelog":null,"contributing":"CONTRIBUTING.adoc","funding":null,"license":"LICENSE.code.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2018-04-27T12:51:10.000Z","updated_at":"2025-05-14T04:24:13.000Z","dependencies_parsed_at":"2025-02-28T03:11:21.419Z","dependency_job_id":"fbd6512c-ac54-4e7f-bb86-944ceeb3cc5e","html_url":"https://github.com/spring-guides/tut-spring-boot-kotlin","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/spring-guides%2Ftut-spring-boot-kotlin","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/spring-guides%2Ftut-spring-boot-kotlin/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/spring-guides%2Ftut-spring-boot-kotlin/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/spring-guides%2Ftut-spring-boot-kotlin/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/spring-guides","download_url":"https://codeload.github.com/spring-guides/tut-spring-boot-kotlin/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":254198452,"owners_count":22030964,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["spring-boot","spring-framework"],"created_at":"2024-09-24T13:55:48.064Z","updated_at":"2025-05-14T18:02:32.558Z","avatar_url":"https://github.com/spring-guides.png","language":"Kotlin","funding_links":[],"categories":[],"sub_categories":[],"readme":":toc:\n:icons: font\n:source-highlighter: prettify\n:project_id: tut-spring-boot-kotlin\n:images: https://raw.githubusercontent.com/spring-guides/tut-spring-boot-kotlin/master/images\n:tabsize: 2\n\nThis tutorial shows you how to build efficiently a sample blog application by combining the power of https://spring.io/projects/spring-boot/[Spring Boot] and https://kotlinlang.org/[Kotlin].\n\nIf you are starting with Kotlin, you can learn the language by reading the https://kotlinlang.org/docs/reference/[reference documentation], following the online https://play.kotlinlang.org/[Kotlin Koans tutorial] or just using https://docs.spring.io/spring/docs/current/spring-framework-reference/[Spring Framework reference documentation] which now provides code samples in Kotlin.\n\nSpring Kotlin support is documented in the https://docs.spring.io/spring/docs/current/spring-framework-reference/languages.html#kotlin[Spring Framework] and https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-kotlin.html[Spring Boot] reference documentation. If you need help, search or ask questions with the https://stackoverflow.com/questions/tagged/kotlin+spring[`spring` and `kotlin` tags on StackOverflow] or come discuss in the `#spring` channel of https://slack.kotlinlang.org/[Kotlin Slack].\n\n== Creating a New Project\n\nFirst we need to create a Spring Boot application, which can be done in a number of ways.\n\n[[using-the-initializr-website]]\n=== Using the Initializr Website\n\nVisit https://start.spring.io and choose the Kotlin language.\nGradle 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.\nNotice that you can use https://start.spring.io/#!language=kotlin\u0026type=gradle-project-kotlin to have Kotlin and Gradle selected by default.\n\n\t. Select \"Gradle - Kotlin\" or \"Maven\" depending on which build tool you want to use\n\t. Enter the following artifact coordinates: `blog`\n\t. Add the following dependencies:\n\t\t- Spring Web\n\t\t- Mustache\n\t\t- Spring Data JPA\n\t\t- H2 Database\n\t\t- Spring Boot DevTools\n\t. Click \"Generate Project\".\n\nThe .zip file contains a standard project in the root directory, so you might want to create an empty directory before you unpack it.\n\n[[using-command-line]]\n=== Using command line\n\nYou can use the Initializr HTTP API https://docs.spring.io/initializr/docs/current/reference/html/#command-line[from the command line] with, for example, curl on a UN*X like system:\n\n[source]\n----\n$ mkdir blog \u0026\u0026 cd blog\n$ curl https://start.spring.io/starter.zip -d language=kotlin -d type=gradle-project-kotlin -d dependencies=web,mustache,jpa,h2,devtools -d packageName=com.example.blog -d name=Blog -o blog.zip\n----\n\nAdd `-d type=gradle-project` if you want to use Gradle.\n\n[[using-intellij-idea]]\n=== Using IntelliJ IDEA\n\nSpring 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.\n\nTo access the wizard, go to File | New | Project, and select Spring Initializr.\n\nFollow the steps of the wizard to use the following parameters:\n\n - Artifact: \"blog\"\n - Type: \"Gradle - Kotlin\" or \"Maven\"\n - Language: Kotlin\n - Name: \"Blog\"\n - Dependencies: \"Spring Web Starter\", \"Mustache\", \"Spring Data JPA\", \"H2 Database\" and \"Spring Boot DevTools\"\n\n[[gradle-build]]\n== Understanding the Gradle Build\n\nIf you're using a Maven Build, you can \u003c\u003cmaven-build, skip to the dedicated section\u003e\u003e.\n\n=== Plugins\n\nIn addition to the obvious https://kotlinlang.org/docs/gradle.html[Kotlin Gradle plugin], the default configuration declares the https://kotlinlang.org/docs/all-open-plugin.html#spring-support[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.\n\nIn order to be able to use Kotlin non-nullable properties with JPA, https://kotlinlang.org/docs/reference/compiler-plugins.html#jpa-support[Kotlin JPA plugin] is also enabled. It generates no-arg constructors for any class annotated with `@Entity`, `@MappedSuperclass` or `@Embeddable`.\n\n\n`build.gradle.kts`\n[source,kotlin]\n----\nimport org.jetbrains.kotlin.gradle.tasks.KotlinCompile\n\nplugins {\n\tid(\"org.springframework.boot\") version \"3.2.2\"\n\tid(\"io.spring.dependency-management\") version \"1.1.4\"\n\tkotlin(\"jvm\") version \"1.9.22\"\n\tkotlin(\"plugin.spring\") version \"1.9.22\"\n\tkotlin(\"plugin.jpa\") version \"1.9.22\"\n}\n----\n\n=== Compiler options\n\nOne of Kotlin's key features is https://kotlinlang.org/docs/null-safety.html[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 https://www.baeldung.com/kotlin/null-safety[comprehensive guide to Kotlin null-safety].\n\nAlthough Java does not allow one to express null-safety in its type-system, Spring Framework provides null-safety of the whole Spring Framework API via tooling-friendly annotations declared in the `org.springframework.lang` package. By default, types from Java APIs used in Kotlin are recognized as https://kotlinlang.org/docs/reference/java-interop.html#null-safety-and-platform-types[platform types] for which null-checks are relaxed. https://kotlinlang.org/docs/java-interop.html#jsr-305-support[Kotlin support for JSR 305 annotations] + Spring nullability annotations provide null-safety for the whole Spring Framework API to Kotlin developers, with the advantage of dealing with `null` related issues at compile time.\n\nThis feature can be enabled by adding the `-Xjsr305` compiler flag with the `strict` options.\n\n`build.gradle.kts`\n[source,kotlin]\n----\ntasks.withType\u003cKotlinCompile\u003e {\n\tkotlinOptions {\n\t\tfreeCompilerArgs += \"-Xjsr305=strict\"\n\t}\n}\n----\n\n=== Dependencies\n\n2 Kotlin specific libraries are required (the standard library is added automatically with Gradle) for such Spring Boot web application and configured by default:\n\n - `kotlin-reflect` is Kotlin reflection library\n - `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)\n\n`build.gradle.kts`\n[source,kotlin]\n----\ndependencies {\n\timplementation(\"org.springframework.boot:spring-boot-starter-data-jpa\")\n\timplementation(\"org.springframework.boot:spring-boot-starter-mustache\")\n\timplementation(\"org.springframework.boot:spring-boot-starter-web\")\n\timplementation(\"com.fasterxml.jackson.module:jackson-module-kotlin\")\n\timplementation(\"org.jetbrains.kotlin:kotlin-reflect\")\n\truntimeOnly(\"com.h2database:h2\")\n\truntimeOnly(\"org.springframework.boot:spring-boot-devtools\")\n\ttestImplementation(\"org.springframework.boot:spring-boot-starter-test\")\n}\n----\n\nRecent versions of H2 require special configuration to properly escape reserved keywords like `user`.\n\n`src/main/resources/application.properties`\n[source,properties]\n----\nspring.jpa.properties.hibernate.globally_quoted_identifiers=true\nspring.jpa.properties.hibernate.globally_quoted_identifiers_skip_column_definitions=true\n----\n\nSpring Boot Gradle plugin automatically uses the Kotlin version declared via the Kotlin Gradle plugin.\n\nYou can now take a \u003c\u003cunderstanding-generated-app, deeper look at the generated application\u003e\u003e.\n\n[[maven-build]]\n== Understanding the Maven Build\n\n=== Plugins\n\nIn addition to the obvious https://kotlinlang.org/docs/reference/using-maven.html[Kotlin Maven plugin], the default configuration declares the https://kotlinlang.org/docs/reference/compiler-plugins.html#spring-support[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.\n\nIn order to be able to use Kotlin non-nullable properties with JPA, https://kotlinlang.org/docs/reference/compiler-plugins.html#jpa-support[Kotlin JPA plugin] is also enabled. It generates no-arg constructors for any class annotated with `@Entity`, `@MappedSuperclass` or `@Embeddable`.\n\n`pom.xml`\n[source,xml]\n----\n\u003cbuild\u003e\n\t\t\u003csourceDirectory\u003e${project.basedir}/src/main/kotlin\u003c/sourceDirectory\u003e\n\t\t\u003ctestSourceDirectory\u003e${project.basedir}/src/test/kotlin\u003c/testSourceDirectory\u003e\n\t\t\u003cplugins\u003e\n\t\t\t\u003cplugin\u003e\n\t\t\t\t\u003cgroupId\u003eorg.springframework.boot\u003c/groupId\u003e\n\t\t\t\t\u003cartifactId\u003espring-boot-maven-plugin\u003c/artifactId\u003e\n\t\t\t\u003c/plugin\u003e\n\t\t\t\u003cplugin\u003e\n\t\t\t\t\u003cgroupId\u003eorg.jetbrains.kotlin\u003c/groupId\u003e\n\t\t\t\t\u003cartifactId\u003ekotlin-maven-plugin\u003c/artifactId\u003e\n\t\t\t\t\u003cconfiguration\u003e\n\t\t\t\t\t\u003ccompilerPlugins\u003e\n\t\t\t\t\t\t\u003cplugin\u003ejpa\u003c/plugin\u003e\n\t\t\t\t\t\t\u003cplugin\u003espring\u003c/plugin\u003e\n\t\t\t\t\t\u003c/compilerPlugins\u003e\n\t\t\t\t\t\u003cargs\u003e\n\t\t\t\t\t\t\u003carg\u003e-Xjsr305=strict\u003c/arg\u003e\n\t\t\t\t\t\u003c/args\u003e\n\t\t\t\t\u003c/configuration\u003e\n\t\t\t\t\u003cdependencies\u003e\n\t\t\t\t\t\u003cdependency\u003e\n\t\t\t\t\t\t\u003cgroupId\u003eorg.jetbrains.kotlin\u003c/groupId\u003e\n\t\t\t\t\t\t\u003cartifactId\u003ekotlin-maven-noarg\u003c/artifactId\u003e\n\t\t\t\t\t\t\u003cversion\u003e${kotlin.version}\u003c/version\u003e\n\t\t\t\t\t\u003c/dependency\u003e\n\t\t\t\t\t\u003cdependency\u003e\n\t\t\t\t\t\t\u003cgroupId\u003eorg.jetbrains.kotlin\u003c/groupId\u003e\n\t\t\t\t\t\t\u003cartifactId\u003ekotlin-maven-allopen\u003c/artifactId\u003e\n\t\t\t\t\t\t\u003cversion\u003e${kotlin.version}\u003c/version\u003e\n\t\t\t\t\t\u003c/dependency\u003e\n\t\t\t\t\u003c/dependencies\u003e\n\t\t\t\u003c/plugin\u003e\n\t\t\u003c/plugins\u003e\n\t\u003c/build\u003e\n----\n\nOne of Kotlin's key features is https://kotlinlang.org/docs/null-safety.html[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 https://www.baeldung.com/kotlin-null-safety[comprehensive guide to Kotlin null-safety].\n\nAlthough Java does not allow one to express null-safety in its type-system, Spring Framework provides null-safety of the whole Spring Framework API via tooling-friendly annotations declared in the `org.springframework.lang` package. By default, types from Java APIs used in Kotlin are recognized as https://kotlinlang.org/docs/reference/java-interop.html#null-safety-and-platform-types[platform types] for which null-checks are relaxed. https://kotlinlang.org/docs/reference/java-interop.html#jsr-305-support[Kotlin support for JSR 305 annotations] + Spring nullability annotations provide null-safety for the whole Spring Framework API to Kotlin developers, with the advantage of dealing with `null` related issues at compile time.\n\nThis feature can be enabled by adding the `-Xjsr305` compiler flag with the `strict` options.\n\nNotice also that Kotlin compiler is configured to generate Java 8 bytecode (Java 6 by default).\n\n=== Dependencies\n\n3 Kotlin specific libraries are required for such Spring Boot web application and configured by default:\n\n - `kotlin-stdlib` is the Kotlin standard library\n - `kotlin-reflect` is Kotlin reflection library\n - `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)\n\n`pom.xml`\n[source,xml]\n----\n\u003cdependencies\u003e\n\t\u003cdependency\u003e\n\t\t\u003cgroupId\u003eorg.springframework.boot\u003c/groupId\u003e\n\t\t\u003cartifactId\u003espring-boot-starter-data-jpa\u003c/artifactId\u003e\n\t\u003c/dependency\u003e\n\t\u003cdependency\u003e\n\t\t\u003cgroupId\u003eorg.springframework.boot\u003c/groupId\u003e\n\t\t\u003cartifactId\u003espring-boot-starter-mustache\u003c/artifactId\u003e\n\t\u003c/dependency\u003e\n\t\u003cdependency\u003e\n\t\t\u003cgroupId\u003eorg.springframework.boot\u003c/groupId\u003e\n\t\t\u003cartifactId\u003espring-boot-starter-web\u003c/artifactId\u003e\n\t\u003c/dependency\u003e\n\t\u003cdependency\u003e\n\t\t\u003cgroupId\u003ecom.fasterxml.jackson.module\u003c/groupId\u003e\n\t\t\u003cartifactId\u003ejackson-module-kotlin\u003c/artifactId\u003e\n\t\u003c/dependency\u003e\n\t\u003cdependency\u003e\n\t\t\u003cgroupId\u003eorg.jetbrains.kotlin\u003c/groupId\u003e\n\t\t\u003cartifactId\u003ekotlin-reflect\u003c/artifactId\u003e\n\t\u003c/dependency\u003e\n\t\u003cdependency\u003e\n\t\t\u003cgroupId\u003eorg.jetbrains.kotlin\u003c/groupId\u003e\n\t\t\u003cartifactId\u003ekotlin-stdlib\u003c/artifactId\u003e\n\t\u003c/dependency\u003e\n\n\t\u003cdependency\u003e\n\t\t\u003cgroupId\u003eorg.springframework.boot\u003c/groupId\u003e\n\t\t\u003cartifactId\u003espring-boot-devtools\u003c/artifactId\u003e\n\t\t\u003cscope\u003eruntime\u003c/scope\u003e\n\t\u003c/dependency\u003e\n\t\u003cdependency\u003e\n\t\t\u003cgroupId\u003ecom.h2database\u003c/groupId\u003e\n\t\t\u003cartifactId\u003eh2\u003c/artifactId\u003e\n\t\t\u003cscope\u003eruntime\u003c/scope\u003e\n\t\u003c/dependency\u003e\n\t\u003cdependency\u003e\n\t\t\u003cgroupId\u003eorg.springframework.boot\u003c/groupId\u003e\n\t\t\u003cartifactId\u003espring-boot-starter-test\u003c/artifactId\u003e\n\t\t\u003cscope\u003etest\u003c/scope\u003e\n\t\u003c/dependency\u003e\n\u003c/dependencies\u003e\n----\n\n[[understanding-generated-app]]\n== Understanding the generated Application\n\n`src/main/kotlin/com/example/blog/BlogApplication.kt`\n[source,kotlin]\n----\npackage com.example.blog\n\nimport org.springframework.boot.autoconfigure.SpringBootApplication\nimport org.springframework.boot.runApplication\n\n@SpringBootApplication\nclass BlogApplication\n\nfun main(args: Array\u003cString\u003e) {\n\trunApplication\u003cBlogApplication\u003e(*args)\n}\n----\n\nCompared 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\u003cBlogApplication\u003e(*args)` is Kotlin idiomatic alternative to `SpringApplication.run(BlogApplication::class.java, *args)` and can be used to customize the application with following syntax.\n\n`src/main/kotlin/com/example/blog/BlogApplication.kt`\n[source,kotlin]\n----\nfun main(args: Array\u003cString\u003e) {\n\trunApplication\u003cBlogApplication\u003e(*args) {\n\t\tsetBannerMode(Banner.Mode.OFF)\n\t}\n}\n----\n\n== Writing your first Kotlin controller\n\nLet's create a simple controller to display a simple web page.\n\n`src/main/kotlin/com/example/blog/HtmlController.kt`\n[source,kotlin]\n----\npackage com.example.blog\n\nimport org.springframework.stereotype.Controller\nimport org.springframework.ui.Model\nimport org.springframework.ui.set\nimport org.springframework.web.bind.annotation.GetMapping\n\n@Controller\nclass HtmlController {\n\n\t@GetMapping(\"/\")\n\tfun blog(model: Model): String {\n\t\tmodel[\"title\"] = \"Blog\"\n\t\treturn \"blog\"\n\t}\n\n}\n----\n\nNotice that we are using here a https://kotlinlang.org/docs/extensions.html[Kotlin extension] that allows to add 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\")`.\nThe https://docs.spring.io/spring-framework/docs/current/kdoc-api/[Spring Framework KDoc API] lists all the Kotlin extensions provided to enrich the Java API.\n\nWe also need to create the associated Mustache templates.\n\n`src/main/resources/templates/header.mustache`\n[source]\n----\n\u003chtml\u003e\n\u003chead\u003e\n\t\u003ctitle\u003e{{title}}\u003c/title\u003e\n\u003c/head\u003e\n\u003cbody\u003e\n----\n\n`src/main/resources/templates/footer.mustache`\n[source]\n----\n\u003c/body\u003e\n\u003c/html\u003e\n----\n\n`src/main/resources/templates/blog.mustache`\n[source]\n----\n{{\u003e header}}\n\n\u003ch1\u003e{{title}}\u003c/h1\u003e\n\n{{\u003e footer}}\n----\n\nStart 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. \n\n== Testing with JUnit 5\n\nJUnit 5 now used by default in Spring Boot provides various features very handy with Kotlin, including https://docs.spring.io/spring/docs/current/spring-framework-reference/testing.html#testcontext-junit-jupiter-di[autowiring of constructor/method parameters] which allows to use non-nullable `val` properties and the possibility to use `@BeforeAll`/`@AfterAll` on regular non-static methods.\n\n=== Writing JUnit 5 tests in Kotlin\n\nFor the sake of this example, let's create an integration test in order to demonstrate various features:\n\n - We use real sentences between backticks instead of camel-case to provide expressive test function names\n - JUnit 5 allows to inject constructor and method parameters, which is a good fit with Kotlin read-only and non-nullable properties\n - This code leverages `getForObject` and `getForEntity` Kotlin extensions (you need to import them)\n\n`src/test/kotlin/com/example/blog/IntegrationTests.kt`\n[source,kotlin]\n----\n@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)\nclass IntegrationTests(@Autowired val restTemplate: TestRestTemplate) {\n\n\t@Test\n\tfun `Assert blog page title, content and status code`() {\n\t\tval entity = restTemplate.getForEntity\u003cString\u003e(\"/\")\n\t\tassertThat(entity.statusCode).isEqualTo(HttpStatus.OK)\n\t\tassertThat(entity.body).contains(\"\u003ch1\u003eBlog\u003c/h1\u003e\")\n\t}\n\n}\n----\n\n=== Test instance lifecycle\n\nSometimes you need to execute a method before or after all tests of a given class. Like Junit 4, JUnit 5 requires by default these methods to be static (which translates to https://kotlinlang.org/docs/object-declarations.html#companion-objects[`companion object`] in Kotlin, which is quite verbose and not straightforward) because test classes are instantiated one time per test.\n\nBut Junit 5 allows you to change this default behavior and instantiate test classes one time per class. This can be done in https://junit.org/junit5/docs/current/user-guide/#writing-tests-test-instance-lifecycle[various ways], here we will use a property file to change the default behavior for the whole project:\n\n`src/test/resources/junit-platform.properties`\n[source,properties]\n----\njunit.jupiter.testinstance.lifecycle.default = per_class\n----\n\nWith this configuration, we can now use `@BeforeAll` and `@AfterAll` annotations on regular methods like shown in updated version of `IntegrationTests` above.\n\n`src/test/kotlin/com/example/blog/IntegrationTests.kt`\n[source,kotlin]\n----\n@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)\nclass IntegrationTests(@Autowired val restTemplate: TestRestTemplate) {\n\n\t@BeforeAll\n\tfun setup() {\n\t\tprintln(\"\u003e\u003e Setup\")\n\t}\n\n\t@Test\n\tfun `Assert blog page title, content and status code`() {\n\t\tprintln(\"\u003e\u003e Assert blog page title, content and status code\")\n\t\tval entity = restTemplate.getForEntity\u003cString\u003e(\"/\")\n\t\tassertThat(entity.statusCode).isEqualTo(HttpStatus.OK)\n\t\tassertThat(entity.body).contains(\"\u003ch1\u003eBlog\u003c/h1\u003e\")\n\t}\n\n\t@Test\n\tfun `Assert article page title, content and status code`() {\n\t\tprintln(\"\u003e\u003e TODO\")\n\t}\n\n\t@AfterAll\n\tfun teardown() {\n\t\tprintln(\"\u003e\u003e Tear down\")\n\t}\n\n}\n----\n\n== Creating your own extensions\n\nInstead 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.\n\n`src/main/kotlin/com/example/blog/Extensions.kt`\n[source,kotlin]\n----\nfun LocalDateTime.format(): String = this.format(englishDateFormatter)\n\nprivate val daysLookup = (1..31).associate { it.toLong() to getOrdinal(it) }\n\nprivate val englishDateFormatter = DateTimeFormatterBuilder()\n\t\t.appendPattern(\"yyyy-MM-dd\")\n\t\t.appendLiteral(\" \")\n\t\t.appendText(ChronoField.DAY_OF_MONTH, daysLookup)\n\t\t.appendLiteral(\" \")\n\t\t.appendPattern(\"yyyy\")\n\t\t.toFormatter(Locale.ENGLISH)\n\nprivate fun getOrdinal(n: Int) = when {\n\tn in 11..13 -\u003e \"${n}th\"\n\tn % 10 == 1 -\u003e \"${n}st\"\n\tn % 10 == 2 -\u003e \"${n}nd\"\n\tn % 10 == 3 -\u003e \"${n}rd\"\n\telse -\u003e \"${n}th\"\n}\n\nfun String.toSlug() = lowercase(Locale.getDefault())\n\t\t.replace(\"\\n\", \" \")\n\t\t.replace(\"[^a-z\\\\d\\\\s]\".toRegex(), \" \")\n\t\t.split(\" \")\n\t\t.joinToString(\"-\")\n\t\t.replace(\"-+\".toRegex(), \"-\")\n----\n\nWe will leverage these extensions in the next section.\n\n== Persistence with JPA\n\nIn order to make lazy fetching working as expected, entities should be `open` as described in https://youtrack.jetbrains.com/issue/KT-28525[KT-28525]. We are going to use the Kotlin `allopen` plugin for that purpose.\n\nWith Gradle:\n\n`build.gradle.kts`\n[source,kotlin]\n----\nplugins {\n\t...\n\tkotlin(\"plugin.allopen\") version \"1.9.22\"\n}\n\nallOpen {\n\tannotation(\"jakarta.persistence.Entity\")\n\tannotation(\"jakarta.persistence.Embeddable\")\n\tannotation(\"jakarta.persistence.MappedSuperclass\")\n}\n----\n\nOr with Maven:\n\n`pom.xml`\n[source,xml]\n----\n\u003cplugin\u003e\n\t\u003cartifactId\u003ekotlin-maven-plugin\u003c/artifactId\u003e\n\t\u003cgroupId\u003eorg.jetbrains.kotlin\u003c/groupId\u003e\n\t\u003cconfiguration\u003e\n\t\t...\n\t\t\u003ccompilerPlugins\u003e\n\t\t\t...\n\t\t\t\u003cplugin\u003eall-open\u003c/plugin\u003e\n\t\t\u003c/compilerPlugins\u003e\n\t\t\u003cpluginOptions\u003e\n\t\t\t\u003coption\u003eall-open:annotation=jakarta.persistence.Entity\u003c/option\u003e\n\t\t\t\u003coption\u003eall-open:annotation=jakarta.persistence.Embeddable\u003c/option\u003e\n\t\t\t\u003coption\u003eall-open:annotation=jakarta.persistence.MappedSuperclass\u003c/option\u003e\n\t\t\u003c/pluginOptions\u003e\n\t\u003c/configuration\u003e\n\u003c/plugin\u003e\n----\n\nThen we create our model by using Kotlin https://kotlinlang.org/docs/reference/classes.html#constructors[primary constructor concise syntax] which allows to declare at the same time the properties and the constructor parameters.\n\n`src/main/kotlin/com/example/blog/Entities.kt`\n[source,kotlin]\n----\n@Entity\nclass Article(\n\t\tvar title: String,\n\t\tvar headline: String,\n\t\tvar content: String,\n\t\t@ManyToOne var author: User,\n\t\tvar slug: String = title.toSlug(),\n\t\tvar addedAt: LocalDateTime = LocalDateTime.now(),\n\t\t@Id @GeneratedValue var id: Long? = null)\n\n@Entity\nclass User(\n\t\tvar login: String,\n\t\tvar firstname: String,\n\t\tvar lastname: String,\n\t\tvar description: String? = null,\n\t\t@Id @GeneratedValue var id: Long? = null)\n----\n\nNotice 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 in order to make it possible to omit them when using positional arguments (Kotlin also supports https://kotlinlang.org/docs/reference/functions.html#named-arguments[named arguments]). Notice that in Kotlin it is not unusual to group concise class declarations in the same file.\n\nNOTE: Here we don't use https://kotlinlang.org/docs/data-classes.html[`data` classes] with `val` properties because JPA is not designed to work with immutable classes or the methods generated automatically by `data` classes. If you are using other Spring Data flavor, most of them are designed to support such constructs so you should use classes like `data class User(val login: String, ...)` when using Spring Data MongoDB, Spring Data JDBC, etc.\n\nNOTE: While Spring Data JPA makes it possible to use natural IDs (it could have been the `login` property in `User` class) via https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#jpa.entity-persistence.saving-entites[`Persistable`], it is not a good fit with Kotlin due to https://youtrack.jetbrains.com/issue/KT-6653[KT-6653], that's why it is recommended to always use entities with generated IDs in Kotlin.\n\n\nWe also declare our Spring Data JPA repositories as following.\n\n`src/main/kotlin/com/example/blog/Repositories.kt`\n[source,kotlin]\n----\ninterface ArticleRepository : CrudRepository\u003cArticle, Long\u003e {\n\tfun findBySlug(slug: String): Article?\n\tfun findAllByOrderByAddedAtDesc(): Iterable\u003cArticle\u003e\n}\n\ninterface UserRepository : CrudRepository\u003cUser, Long\u003e {\n\tfun findByLogin(login: String): User?\n}\n----\n\nAnd we write JPA tests to check whether basic use cases work as expected.\n\n`src/test/kotlin/com/example/blog/RepositoriesTests.kt`\n[source,kotlin]\n----\n@DataJpaTest\nclass RepositoriesTests @Autowired constructor(\n\t\tval entityManager: TestEntityManager,\n\t\tval userRepository: UserRepository,\n\t\tval articleRepository: ArticleRepository) {\n\n\t@Test\n\tfun `When findByIdOrNull then return Article`() {\n\t\tval johnDoe = User(\"johnDoe\", \"John\", \"Doe\")\n\t\tentityManager.persist(johnDoe)\n\t\tval article = Article(\"Lorem\", \"Lorem\", \"dolor sit amet\", johnDoe)\n\t\tentityManager.persist(article)\n\t\tentityManager.flush()\n\t\tval found = articleRepository.findByIdOrNull(article.id!!)\n\t\tassertThat(found).isEqualTo(article)\n\t}\n\n\t@Test\n\tfun `When findByLogin then return User`() {\n\t\tval johnDoe = User(\"johnDoe\", \"John\", \"Doe\")\n\t\tentityManager.persist(johnDoe)\n\t\tentityManager.flush()\n\t\tval user = userRepository.findByLogin(johnDoe.login)\n\t\tassertThat(user).isEqualTo(johnDoe)\n\t}\n}\n----\n\nNOTE: 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 https://medium.com/@elizarov/null-is-your-friend-not-a-mistake-b63ff1751dd5[Null is your friend, not a mistake] blog post for more details.\n\n== Implementing the blog engine\n\nWe update the \"blog\" Mustache templates.\n\n`src/main/resources/templates/blog.mustache`\n[source]\n----\n{{\u003e header}}\n\n\u003ch1\u003e{{title}}\u003c/h1\u003e\n\n\u003cdiv class=\"articles\"\u003e\n\n\t{{#articles}}\n\t\t\u003csection\u003e\n\t\t\t\u003cheader class=\"article-header\"\u003e\n\t\t\t\t\u003ch2 class=\"article-title\"\u003e\u003ca href=\"/article/{{slug}}\"\u003e{{title}}\u003c/a\u003e\u003c/h2\u003e\n\t\t\t\t\u003cdiv class=\"article-meta\"\u003eBy  \u003cstrong\u003e{{author.firstname}}\u003c/strong\u003e, on \u003cstrong\u003e{{addedAt}}\u003c/strong\u003e\u003c/div\u003e\n\t\t\t\u003c/header\u003e\n\t\t\t\u003cdiv class=\"article-description\"\u003e\n\t\t\t\t{{headline}}\n\t\t\t\u003c/div\u003e\n\t\t\u003c/section\u003e\n\t{{/articles}}\n\u003c/div\u003e\n\n{{\u003e footer}}\n----\n\nAnd we create an \"article\" new one.\n\n`src/main/resources/templates/article.mustache`\n[source]\n----\n{{\u003e header}}\n\n\u003csection class=\"article\"\u003e\n\t\u003cheader class=\"article-header\"\u003e\n\t\t\u003ch1 class=\"article-title\"\u003e{{article.title}}\u003c/h1\u003e\n\t\t\u003cp class=\"article-meta\"\u003eBy  \u003cstrong\u003e{{article.author.firstname}}\u003c/strong\u003e, on \u003cstrong\u003e{{article.addedAt}}\u003c/strong\u003e\u003c/p\u003e\n\t\u003c/header\u003e\n\n\t\u003cdiv class=\"article-description\"\u003e\n\t\t{{article.headline}}\n\n\t\t{{article.content}}\n\t\u003c/div\u003e\n\u003c/section\u003e\n\n{{\u003e footer}}\n----\n\nWe update the `HtmlController` in order to render blog and article pages with the formatted date. `ArticleRepository` constructor parameter will be automatically autowired since `HtmlController` has a single constructor (implicit `@Autowired`).\n\n`src/main/kotlin/com/example/blog/HtmlController.kt`\n[source,kotlin]\n----\n@Controller\nclass HtmlController(private val repository: ArticleRepository) {\n\n\t@GetMapping(\"/\")\n\tfun blog(model: Model): String {\n\t\tmodel[\"title\"] = \"Blog\"\n\t\tmodel[\"articles\"] = repository.findAllByOrderByAddedAtDesc().map { it.render() }\n\t\treturn \"blog\"\n\t}\n\n\t@GetMapping(\"/article/{slug}\")\n\tfun article(@PathVariable slug: String, model: Model): String {\n\t\tval article = repository\n\t\t\t\t.findBySlug(slug)\n\t\t\t\t?.render()\n\t\t\t\t?: throw ResponseStatusException(HttpStatus.NOT_FOUND, \"This article does not exist\")\n\t\tmodel[\"title\"] = article.title\n\t\tmodel[\"article\"] = article\n\t\treturn \"article\"\n\t}\n\n\tfun Article.render() = RenderedArticle(\n\t\t\tslug,\n\t\t\ttitle,\n\t\t\theadline,\n\t\t\tcontent,\n\t\t\tauthor,\n\t\t\taddedAt.format()\n\t)\n\n\tdata class RenderedArticle(\n\t\t\tval slug: String,\n\t\t\tval title: String,\n\t\t\tval headline: String,\n\t\t\tval content: String,\n\t\t\tval author: User,\n\t\t\tval addedAt: String)\n\n}\n----\n\nThen, we add data initialization to a new `BlogConfiguration` class.\n\n`src/main/kotlin/com/example/blog/BlogConfiguration.kt`\n[source,kotlin]\n----\n@Configuration\nclass BlogConfiguration {\n\n\t@Bean\n\tfun databaseInitializer(userRepository: UserRepository,\n\t\t\t\t\t\t\tarticleRepository: ArticleRepository) = ApplicationRunner {\n\n\t\tval johnDoe = userRepository.save(User(\"johnDoe\", \"John\", \"Doe\"))\n\t\tarticleRepository.save(Article(\n\t\t\t\ttitle = \"Lorem\",\n\t\t\t\theadline = \"Lorem\",\n\t\t\t\tcontent = \"dolor sit amet\",\n\t\t\t\tauthor = johnDoe\n\t\t))\n\t\tarticleRepository.save(Article(\n\t\t\t\ttitle = \"Ipsum\",\n\t\t\t\theadline = \"Ipsum\",\n\t\t\t\tcontent = \"dolor sit amet\",\n\t\t\t\tauthor = johnDoe\n\t\t))\n\t}\n}\n----\n\nNOTE: Notice the usage of named parameters to make the code more readable.\n\nAnd we also update the integration tests accordingly.\n\n`src/test/kotlin/com/example/blog/IntegrationTests.kt`\n[source,kotlin]\n----\n@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)\nclass IntegrationTests(@Autowired val restTemplate: TestRestTemplate) {\n\n\t@BeforeAll\n\tfun setup() {\n\t\tprintln(\"\u003e\u003e Setup\")\n\t}\n\n\t@Test\n\tfun `Assert blog page title, content and status code`() {\n\t\tprintln(\"\u003e\u003e Assert blog page title, content and status code\")\n\t\tval entity = restTemplate.getForEntity\u003cString\u003e(\"/\")\n\t\tassertThat(entity.statusCode).isEqualTo(HttpStatus.OK)\n\t\tassertThat(entity.body).contains(\"\u003ch1\u003eBlog\u003c/h1\u003e\", \"Lorem\")\n\t}\n\n\t@Test\n\tfun `Assert article page title, content and status code`() {\n\t\tprintln(\"\u003e\u003e Assert article page title, content and status code\")\n\t\tval title = \"Lorem\"\n\t\tval entity = restTemplate.getForEntity\u003cString\u003e(\"/article/${title.toSlug()}\")\n\t\tassertThat(entity.statusCode).isEqualTo(HttpStatus.OK)\n\t\tassertThat(entity.body).contains(title, \"Lorem\", \"dolor sit amet\")\n\t}\n\n\t@AfterAll\n\tfun teardown() {\n\t\tprintln(\"\u003e\u003e Tear down\")\n\t}\n\n}\n----\n\nStart (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.\n\n== Exposing HTTP API\n\nWe are now going to implement the HTTP API via `@RestController` annotated controllers.\n\n`src/main/kotlin/com/example/blog/HttpControllers.kt`\n[source,kotlin]\n----\n@RestController\n@RequestMapping(\"/api/article\")\nclass ArticleController(private val repository: ArticleRepository) {\n\n\t@GetMapping(\"/\")\n\tfun findAll() = repository.findAllByOrderByAddedAtDesc()\n\n\t@GetMapping(\"/{slug}\")\n\tfun findOne(@PathVariable slug: String) =\n\t\t\trepository.findBySlug(slug) ?: throw ResponseStatusException(HttpStatus.NOT_FOUND, \"This article does not exist\")\n\n}\n\n@RestController\n@RequestMapping(\"/api/user\")\nclass UserController(private val repository: UserRepository) {\n\n\t@GetMapping(\"/\")\n\tfun findAll() = repository.findAll()\n\n\t@GetMapping(\"/{login}\")\n\tfun findOne(@PathVariable login: String) =\n\t\t\trepository.findByLogin(login) ?: throw ResponseStatusException(HttpStatus.NOT_FOUND, \"This user does not exist\")\n}\n----\n\nFor tests, instead of integration tests, we are going to leverage `@WebMvcTest` and https://mockk.io/[Mockk] which is similar to https://site.mockito.org/[Mockito] but better suited for Kotlin.\n\nSince `@MockBean` and `@SpyBean` annotations are specific to Mockito, we are going to leverage https://github.com/Ninja-Squad/springmockk[SpringMockK] which provides similar `@MockkBean` and `@SpykBean` annotations for Mockk.\n\nWith Gradle:\n\n`build.gradle.kts`\n[source,kotlin]\n----\ntestImplementation(\"org.springframework.boot:spring-boot-starter-test\") {\n\texclude(module = \"mockito-core\")\n}\ntestImplementation(\"org.junit.jupiter:junit-jupiter-api\")\ntestRuntimeOnly(\"org.junit.jupiter:junit-jupiter-engine\")\ntestImplementation(\"com.ninja-squad:springmockk:4.0.2\")\n----\n\nOr with Maven:\n\n`pom.xml`\n[source,xml]\n----\n\u003cdependency\u003e\n\t\u003cgroupId\u003eorg.springframework.boot\u003c/groupId\u003e\n\t\u003cartifactId\u003espring-boot-starter-test\u003c/artifactId\u003e\n\t\u003cscope\u003etest\u003c/scope\u003e\n\u003c/dependency\u003e\n\u003cdependency\u003e\n\t\u003cgroupId\u003eorg.junit.jupiter\u003c/groupId\u003e\n\t\u003cartifactId\u003ejunit-jupiter-engine\u003c/artifactId\u003e\n\t\u003cscope\u003etest\u003c/scope\u003e\n\u003c/dependency\u003e\n\u003cdependency\u003e\n\t\u003cgroupId\u003ecom.ninja-squad\u003c/groupId\u003e\n\t\u003cartifactId\u003espringmockk\u003c/artifactId\u003e\n\t\u003cversion\u003e4.0.2\u003c/version\u003e\n\t\u003cscope\u003etest\u003c/scope\u003e\n\u003c/dependency\u003e\n----\n\n`src/test/kotlin/com/example/blog/HttpControllersTests.kt`\n[source,kotlin]\n----\n@WebMvcTest\nclass HttpControllersTests(@Autowired val mockMvc: MockMvc) {\n\n\t@MockkBean\n\tlateinit var userRepository: UserRepository\n\n\t@MockkBean\n\tlateinit var articleRepository: ArticleRepository\n\n\t@Test\n\tfun `List articles`() {\n\t\tval johnDoe = User(\"johnDoe\", \"John\", \"Doe\")\n\t\tval lorem5Article = Article(\"Lorem\", \"Lorem\", \"dolor sit amet\", johnDoe)\n\t\tval ipsumArticle = Article(\"Ipsum\", \"Ipsum\", \"dolor sit amet\", johnDoe)\n\t\tevery { articleRepository.findAllByOrderByAddedAtDesc() } returns listOf(lorem5Article, ipsumArticle)\n\t\tmockMvc.perform(get(\"/api/article/\").accept(MediaType.APPLICATION_JSON))\n\t\t\t\t.andExpect(status().isOk)\n\t\t\t\t.andExpect(content().contentType(MediaType.APPLICATION_JSON))\n\t\t\t\t.andExpect(jsonPath(\"\\$.[0].author.login\").value(johnDoe.login))\n\t\t\t\t.andExpect(jsonPath(\"\\$.[0].slug\").value(lorem5Article.slug))\n\t\t\t\t.andExpect(jsonPath(\"\\$.[1].author.login\").value(johnDoe.login))\n\t\t\t\t.andExpect(jsonPath(\"\\$.[1].slug\").value(ipsumArticle.slug))\n\t}\n\n\t@Test\n\tfun `List users`() {\n\t\tval johnDoe = User(\"johnDoe\", \"John\", \"Doe\")\n\t\tval janeDoe = User(\"janeDoe\", \"Jane\", \"Doe\")\n\t\tevery { userRepository.findAll() } returns listOf(johnDoe, janeDoe)\n\t\tmockMvc.perform(get(\"/api/user/\").accept(MediaType.APPLICATION_JSON))\n\t\t\t\t.andExpect(status().isOk)\n\t\t\t\t.andExpect(content().contentType(MediaType.APPLICATION_JSON))\n\t\t\t\t.andExpect(jsonPath(\"\\$.[0].login\").value(johnDoe.login))\n\t\t\t\t.andExpect(jsonPath(\"\\$.[1].login\").value(janeDoe.login))\n\t}\n}\n----\n\nNOTE: `$` needs to be escaped in strings as it is used for string interpolation.\n\n== Configuration properties\n\nIn Kotlin, the recommended way to manage your application properties is to use read-only properties.\n\n`src/main/kotlin/com/example/blog/BlogProperties.kt`\n[source,kotlin]\n----\n@ConfigurationProperties(\"blog\")\ndata class BlogProperties(var title: String, val banner: Banner) {\n\tdata class Banner(val title: String? = null, val content: String)\n}\n----\n\nThen we enable it at `BlogApplication` level.\n\n`src/main/kotlin/com/example/blog/BlogApplication.kt`\n[source,kotlin]\n----\n@SpringBootApplication\n@EnableConfigurationProperties(BlogProperties::class)\nclass BlogApplication {\n\t// ...\n}\n----\n\nTo generate https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#configuration-metadata-annotation-processor[your own metadata] in order to get these custom properties recognized by your IDE, https://kotlinlang.org/docs/reference/kapt.html[kapt should be configured] with the `spring-boot-configuration-processor` dependency as following.\n\n`build.gradle.kts`\n[source,kotlin]\n----\nplugins {\n\t...\n\tkotlin(\"kapt\") version \"1.9.22\"\n}\n\ndependencies {\n\t...\n\tkapt(\"org.springframework.boot:spring-boot-configuration-processor\")\n}\n----\n\nNOTE: Note that some features (such as detecting the default value or deprecated items) are not working due to limitations in the model kapt provides. Also annotation processing is not yet supported with Maven due to https://youtrack.jetbrains.com/issue/KT-18022[KT-18022], see https://github.com/spring-io/initializr/issues/438[initializr#438] for more details.\n\nIn IntelliJ IDEA:\n\n - Make sure Spring Boot plugin in enabled in menu File | Settings | Plugins | Spring Boot\n - Enable annotation processing via menu File | Settings | Build, Execution, Deployment | Compiler | Annotation Processors | Enable annotation processing\n - Since https://youtrack.jetbrains.com/issue/KT-15040[Kapt is not yet integrated in IDEA], you need to run manually the command `./gradlew kaptKotlin` to generate the metadata\n\nYour custom properties should now be recognized when editing `application.properties` (autocomplete, validation, etc.).\n\n`src/main/resources/application.properties`\n[source,properties]\n----\nblog.title=Blog\nblog.banner.title=Warning\nblog.banner.content=The blog will be down tomorrow.\n----\n\nEdit the template and the controller accordingly.\n\n`src/main/resources/templates/blog.mustache`\n[source]\n----\n{{\u003e header}}\n\n\u003cdiv class=\"articles\"\u003e\n\n\t{{#banner.title}}\n\t\u003csection\u003e\n\t\t\u003cheader class=\"banner\"\u003e\n\t\t\t\u003ch2 class=\"banner-title\"\u003e{{banner.title}}\u003c/h2\u003e\n\t\t\u003c/header\u003e\n\t\t\u003cdiv class=\"banner-content\"\u003e\n\t\t\t{{banner.content}}\n\t\t\u003c/div\u003e\n\t\u003c/section\u003e\n\t{{/banner.title}}\n\n\t...\n\n\u003c/div\u003e\n\n{{\u003e footer}}\n----\n\n`src/main/kotlin/com/example/blog/HtmlController.kt`\n[source,kotlin]\n----\n@Controller\nclass HtmlController(private val repository: ArticleRepository,\n\t\t\t\t\t private val properties: BlogProperties) {\n\n\t@GetMapping(\"/\")\n\tfun blog(model: Model): String {\n\t\tmodel[\"title\"] = properties.title\n\t\tmodel[\"banner\"] = properties.banner\n\t\tmodel[\"articles\"] = repository.findAllByOrderByAddedAtDesc().map { it.render() }\n\t\treturn \"blog\"\n\t}\n\n\t// ...\n----\n\nRestart the web application, refresh `http://localhost:8080/`, you should see the banner on the blog homepage.\n\n== Conclusion\n\nWe have now finished to build this sample Kotlin blog application. The source code https://github.com/spring-guides/tut-spring-boot-kotlin[is available on Github]. You can also have a look to https://docs.spring.io/spring/docs/current/spring-framework-reference/languages.html#kotlin[Spring Framework] and https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-kotlin.html[Spring Boot] reference documentation if you need more details on specific features.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fspring-guides%2Ftut-spring-boot-kotlin","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fspring-guides%2Ftut-spring-boot-kotlin","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fspring-guides%2Ftut-spring-boot-kotlin/lists"}