Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions codeSnippets/settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@ module("snippets", "tutorial-server-docker-compose")
module("snippets", "htmx-integration")
module("snippets", "server-http-request-lifecycle")
module("snippets", "openapi-spec-gen")
module("snippets", "server-di")

if(!System.getProperty("os.name").startsWith("Windows")) {
module("snippets", "embedded-server-native")
Expand Down
12 changes: 12 additions & 0 deletions codeSnippets/snippets/server-di/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Ktor Dependency Injection sample

This project demonstrates the usage of Ktor’s built-in Dependency Injection (DI) plugin.
> This sample is a part of the [codeSnippets](../../README.md) Gradle project.

## Running the Project

```bash
./gradlew :server-di:run
```

Then, navigate to [http://localhost:8080/greet/world](http://localhost:8080/greet/world).
29 changes: 29 additions & 0 deletions codeSnippets/snippets/server-di/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
val ktor_version: String by project
val kotlin_version: String by project
val logback_version: String by project

plugins {
application
kotlin("jvm")
}

application {
mainClass.set("io.ktor.server.netty.EngineMain")
}

repositories {
mavenCentral()
maven { url = uri("https://maven.pkg.jetbrains.space/public/p/ktor/eap") }
}

dependencies {
implementation("io.ktor:ktor-server-core-jvm:$ktor_version")
implementation("io.ktor:ktor-server-di:$ktor_version")
implementation("io.ktor:ktor-server-netty-jvm:$ktor_version")
implementation("io.ktor:ktor-server-config-yaml:${ktor_version}")
implementation("io.ktor:ktor-server-content-negotiation-jvm:$ktor_version")
implementation("io.ktor:ktor-serialization-kotlinx-json-jvm:$ktor_version")
implementation("ch.qos.logback:logback-classic:${logback_version}")
testImplementation("io.ktor:ktor-server-test-host-jvm:$ktor_version")
testImplementation(kotlin("test"))
}
22 changes: 22 additions & 0 deletions codeSnippets/snippets/server-di/requests.http
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
@host = http://localhost:8080

### Successful checkout using cookies and amount
POST http://localhost:8080/checkout?amount=1500
Content-Type: application/x-www-form-urlencoded
Cookie: userId=alice; cartId=cart123
Comment on lines +1 to +6
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

@host variable defined but never used; cartId value inconsistent across requests.

The @host variable on line 1 is never referenced — all requests hardcode http://localhost:8080. Either use {{host}}/checkout... in the request URLs or remove the variable.

Also, the successful checkout (line 6) uses cartId=cart123 while the other two scenarios (lines 13, 20) use cartId=cart-123. Consider making the format consistent to avoid confusing readers of the sample.

Proposed fix
-@host = http://localhost:8080
-
 ### Successful checkout using cookies and amount
-POST http://localhost:8080/checkout?amount=1500
+POST {{host}}/checkout?amount=1500
 Content-Type: application/x-www-form-urlencoded
-Cookie: userId=alice; cartId=cart123
+Cookie: userId=alice; cartId=cart-123
 
 ###
 
 ### Missing userId cookie
-POST http://localhost:8080/checkout?amount=1500
+POST {{host}}/checkout?amount=1500
 Content-Type: application/x-www-form-urlencoded
 Cookie: cartId=cart-123
 
 ###
 
 ### Missing amount query parameter
-POST http://localhost:8080/checkout
+POST {{host}}/checkout
 Content-Type: application/x-www-form-urlencoded
 Cookie: userId=alice; cartId=cart-123
🤖 Prompt for AI Agents
In `@codeSnippets/snippets/server-di/requests.http` around lines 1 - 6, The `@host`
variable is defined but unused and the cartId values are inconsistent; update
the HTTP requests to use the host variable (replace hardcoded
"http://localhost:8080" with "{{host}}" in each request URL) or delete the `@host`
line if you prefer hardcoding, and make all Cookie headers use a consistent
cartId format (e.g., change "cart123" to "cart-123" in the successful checkout
request) so the Cookie header and all request URLs (the POST /checkout requests)
are consistent.


###

### Missing userId cookie
POST http://localhost:8080/checkout?amount=1500
Content-Type: application/x-www-form-urlencoded
Cookie: cartId=cart-123

###

### Missing amount query parameter
POST http://localhost:8080/checkout
Content-Type: application/x-www-form-urlencoded
Cookie: userId=alice; cartId=cart-123

###
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.example

import io.ktor.server.application.Application
import io.ktor.server.plugins.di.dependencies
import io.ktor.server.response.respondText
import io.ktor.server.routing.get
import io.ktor.server.routing.routing

fun main(args: Array<String>): Unit = io.ktor.server.netty.EngineMain.main(args)

fun Application.module(
greetingService: GreetingService,
userRepository: UserRepository,
) {
routing {
val optional: OptionalConfig? by dependencies

get("/greet/{name}") {
val name = call.parameters["name"] ?: "World"
call.respondText(greetingService.greet(name))
}

get("/db") {
call.respondText("DB = ${userRepository.db}")
}

get("/optional") {
call.respondText("Optional = $optional")
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.example

import io.ktor.server.application.Application
import io.ktor.server.application.log
import io.ktor.server.plugins.di.dependencies
import kotlinx.coroutines.delay

data class EventsConnection(val connected: Boolean)

suspend fun Application.installEvents() {
val conn: EventsConnection = dependencies.resolve()
log.info("Events connection ready: $conn")
}

suspend fun Application.loadEventsConnection() {
dependencies.provide {
delay(200) // simulate async work
EventsConnection(true)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.example

interface Database

class PostgresDatabase(val url: String) : Database
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.example

interface GreetingService {
fun greet(name: String): String
}

class GreetingServiceImpl : GreetingService {
override fun greet(name: String): String = "Hello, $name!"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.example

import io.ktor.server.application.*
import io.ktor.server.plugins.di.dependencies
import java.io.PrintStream

class Logger(private val out: PrintStream) {
fun log(message: String) {
out.println("[LOG] $message")
}
}

fun Application.logging(printStreamProvider: () -> PrintStream) {
dependencies {
provide<Logger> {
Logger(printStreamProvider())
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package com.example

data class OptionalConfig(val value: String)
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package com.example

import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.plugins.di.annotations.*
import io.ktor.server.plugins.di.dependencies
import io.ktor.server.response.*
import io.ktor.server.routing.*

interface PaymentProcessor {
suspend fun handlePayment(call: ApplicationCall, userId: String, cartId: String, amount: Long)
}
class CreditCardPaymentProvider(
val baseUrl: String,
val clientKey: String,
val hashEncoding: (String) -> String
) : PaymentProcessor {
override suspend fun handlePayment(call: ApplicationCall, userId: String, cartId: String, amount: Long) {
call.response.header("X-Transaction-Id", "$userId:$cartId")
call.response.header("X-Digest", hashEncoding("$clientKey:$userId:$cartId:$amount"))
call.respondRedirect("$baseUrl/payment/$amount")
}
}

class PointsBalancePaymentProvider(
val updatePoints: suspend (String, Long) -> Long
) : PaymentProcessor {
override suspend fun handlePayment(call: ApplicationCall, userId: String, cartId: String, amount: Long) {
updatePoints(userId, amount)
call.respondRedirect("/paymentComplete?userId=$userId&cartId=$cartId&amount=$amount")
}
}

fun Application.configureExternalPaymentProvider(
@Property("payments.url") baseUrl: String,
@Property("payments.clientKey") clientKey: String,
) {
dependencies {
provide("external") { CreditCardPaymentProvider(baseUrl, clientKey) { it.reversed() } }
}
}


fun Application.paymentsHandling(
@Named("external") payments: PaymentProcessor
) {
log.info("Using payment processor: $payments")
routing {
post("/checkout") {
val userId = call.request.cookies["userId"]
?: return@post call.respondText("Login required", status = HttpStatusCode.Forbidden)
val cartId = call.request.cookies["cartId"]
?: return@post call.respondText("Cart ID missing", status = HttpStatusCode.Forbidden)
val amount = call.request.queryParameters["amount"]?.toLongOrNull() ?: return@post call.respondText("Amount missing", status = HttpStatusCode.BadRequest)

payments.handlePayment(call, userId, cartId, amount)
}
get("/paymment/{amount}") {
call.respondText("Payment for ${call.parameters["amount"]} is pending...")
}
Comment on lines +58 to +60
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Typo in route path: /paymment/payment.

Double 'm' in the route path.

Proposed fix
-        get("/paymment/{amount}") {
+        get("/payment/{amount}") {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
get("/paymment/{amount}") {
call.respondText("Payment for ${call.parameters["amount"]} is pending...")
}
get("/payment/{amount}") {
call.respondText("Payment for ${call.parameters["amount"]} is pending...")
}
🤖 Prompt for AI Agents
In
`@codeSnippets/snippets/server-di/src/main/kotlin/com.example/PaymentService.kt`
around lines 58 - 60, The route path in the GET handler is misspelled as
"/paymment/{amount}"; update the route string used in the get(...) call inside
PaymentService (the GET handler that calls call.respondText with
call.parameters["amount"]) to "/payment/{amount}" and verify any tests or
callers referencing "/paymment" are updated to the corrected "/payment" path.

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.example

import java.io.PrintStream

fun stdout(): () -> PrintStream = { System.out }
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.example

import io.ktor.server.plugins.di.annotations.Property

fun provideDatabase(
@Property("database.connectionUrl") connectionUrl: String
): Database = PostgresDatabase(connectionUrl)

open class UserRepository(val db: Database)
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
ktor:
deployment:
port: 8080
application:
dependencies:
- com.example.RepositoriesKt.provideDatabase
- com.example.UserRepository
- com.example.GreetingServiceImpl
- com.example.PrintStreamProviderKt.stdout
modules:
- com.example.ApplicationKt.module
- com.example.LoggingKt.logging
- com.example.PaymentServiceKt.configureExternalPaymentProvider
- com.example.PaymentServiceKt.paymentsHandling
database:
connectionUrl: postgres://localhost:3037/admin
Comment on lines +15 to +16
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Non-standard PostgreSQL port in sample configuration.

Port 3037 is unusual for PostgreSQL (default is 5432). If this is intentional for demonstration purposes, consider adding a YAML comment to avoid confusing readers who may copy this config.

🤖 Prompt for AI Agents
In `@codeSnippets/snippets/server-di/src/main/resources/application.yaml` around
lines 13 - 14, The sample database connectionUrl uses a non-standard PostgreSQL
port (postgres://localhost:3037/admin); either change the port to the standard
5432 in the database.connectionUrl value or add a YAML comment next to
database.connectionUrl explaining that 3037 is intentionally non-standard for
demo purposes so readers aren't confused when copying the config.


connection:
domain: api.example.com
path: /v1
protocol: https

payments:
url: "http://localhost:8080"
clientKey: "super-secret-client-key"
29 changes: 29 additions & 0 deletions codeSnippets/snippets/server-di/src/test/kotlin/GreetingTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.example

import io.ktor.client.request.get
import io.ktor.client.statement.bodyAsText
import io.ktor.server.testing.*
import kotlin.test.*

class GreetingTest {
@Test
fun testGreeting() = testApplication {
application {
module(
greetingService = FakeGreetingService(),
userRepository = FakeUserRepository(),
)
}

val response = client.get("/greet/Test")
assertEquals("Fake greeting", response.bodyAsText())
}
}

class FakeGreetingService : GreetingService {
override fun greet(name: String) = "Fake greeting"
}

class FakeUserRepository : UserRepository(FakeDatabase())

class FakeDatabase : Database
9 changes: 8 additions & 1 deletion ktor.tree
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,14 @@
accepts-web-file-names="configuration.html,configurations.html,environments.html,configuration-file.html"/>
<toc-element topic="server-modules.md"
accepts-web-file-names="modules.html"/>
<toc-element topic="server-dependency-injection.md"/>
<toc-element toc-title="Dependency injection">
<toc-element topic="server-dependency-injection.md" toc-title="Overview"/>
<toc-element topic="server-di-configuration.md" toc-title="Configuration"/>
<toc-element topic="server-di-dependency-registration.md"/>
<toc-element topic="server-di-dependency-resolution.md"/>
<toc-element topic="server-di-resource-lifecycle-management.md"/>
<toc-element topic="server-di-testing.md" toc-title="Testing"/>
</toc-element>
<toc-element topic="server-plugins.md"
toc-title="Plugins"
accepts-web-file-names="zfeatures.html,features.html,plugins.html"/>
Expand Down
Loading