Ktor Backend
Ktor is a lightweight, and asynchronous framework developed by JetBrains for building web applications, HTTP services, and mobile backends using Kotlin.
1. What is Ktor?
Ktor is a backend framework made for Kotlin.
You can use Ktor to build:
- REST APIs
- Authentication systems
- Admin panels
- Mobile app backends
- File upload servers
- Payment APIs
- WebSocket chat apps
- Microservices
- Internal company tools
- AI API wrapper backends
Simple meaning:
1
2
3
4
5
6
7
Android App / iOS App / Website
↓
API Call
↓
Ktor Backend
↓
Database / External API / File Storage
Example:
1
2
3
4
5
6
7
User clicks Login in Android app
↓
Android sends email/password to Ktor API
↓
Ktor checks database
↓
Ktor returns success + token
2. Why Learn Ktor Backend?
Ktor is good for Android developers because it uses Kotlin.
If you already know Kotlin for Android, Ktor feels easier than learning a fully different backend language.
Benefits:
- Kotlin-based
- Lightweight
- Fast
- Simple routing
- Good for REST API
- Good for microservices
- Supports JSON easily
- Supports JWT authentication
- Supports WebSocket
- Works with databases
- Easy to test
- Good for mobile app backend
3. Basic Backend Concepts Before Ktor
Before learning Ktor, understand these backend basics.
Server
A server is a program that waits for requests and sends responses.
Example:
1
2
3
4
5
6
7
8
9
10
Client Request:
GET /users
Server Response:
[
{
"id": 1,
"name": "Envobyte"
}
]
Client
A client is anything that sends requests to the backend.
Examples:
- Android app
- iOS app
- Website
- Postman
- Browser
- Another backend server
API
API means Application Programming Interface.
In simple words, an API is a way for apps to communicate with a backend.
Example API endpoints:
1
2
3
4
5
GET /users
POST /login
POST /orders
GET /products
DELETE /notes/1
Request
A request is sent from client to server.
Example:
1
2
3
4
5
6
7
POST /login
Content-Type: application/json
{
"email": "test@example.com",
"password": "123456"
}
Response
A response is sent from server to client.
Example:
1
2
3
4
{
"success": true,
"token": "jwt-token-here"
}
HTTP Methods
Common HTTP methods:
1
2
3
4
5
GET Read data
POST Create data
PUT Replace data
PATCH Update part of data
DELETE Delete data
Example:
1
2
3
4
5
GET /products Get all products
GET /products/1 Get single product
POST /products Create product
PUT /products/1 Update product
DELETE /products/1 Delete product
Status Codes
Status codes tell the client what happened.
1
2
3
4
5
6
7
8
200 OK Request successful
201 Created Data created
400 Bad Request Wrong input
401 Unauthorized Not logged in
403 Forbidden No permission
404 Not Found Data not found
409 Conflict Duplicate/conflict
500 Internal Server Error Server problem
4. Ktor Project Structure
A simple Ktor project can look like this:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
ktor-backend/
│
├── build.gradle.kts
├── settings.gradle.kts
├── src/
│ ├── main/
│ │ ├── kotlin/
│ │ │ └── com/example/
│ │ │ ├── Application.kt
│ │ │ ├── routes/
│ │ │ │ ├── UserRoutes.kt
│ │ │ │ └── AuthRoutes.kt
│ │ │ ├── models/
│ │ │ │ └── User.kt
│ │ │ ├── dto/
│ │ │ │ └── LoginRequest.kt
│ │ │ ├── repository/
│ │ │ │ └── UserRepository.kt
│ │ │ ├── service/
│ │ │ │ └── AuthService.kt
│ │ │ ├── database/
│ │ │ │ └── DatabaseFactory.kt
│ │ │ └── plugins/
│ │ │ ├── Routing.kt
│ │ │ ├── Serialization.kt
│ │ │ ├── Security.kt
│ │ │ └── Monitoring.kt
│ │ │
│ │ └── resources/
│ │ ├── application.conf
│ │ └── logback.xml
│ │
│ └── test/
│ └── kotlin/
│ └── ApplicationTest.kt
Simple explanation:
1
2
3
4
5
6
7
8
9
10
Application.kt Main entry point
routes/ API endpoints
models/ Main data models
dto/ Request/response models
repository/ Database/data access logic
service/ Business logic
database/ Database connection
plugins/ Ktor plugin setup
resources/ Config files
test/ Backend tests
5. Gradle Setup
Ktor uses Gradle to manage dependencies and build the backend.
Example build.gradle.kts:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
plugins {
kotlin("jvm") version "2.0.21"
id("io.ktor.plugin") version "3.5.0"
kotlin("plugin.serialization") version "2.0.21"
}
group = "com.example"
version = "1.0.0"
application {
mainClass.set("io.ktor.server.netty.EngineMain")
}
repositories {
mavenCentral()
}
dependencies {
implementation("io.ktor:ktor-server-core")
implementation("io.ktor:ktor-server-netty")
implementation("io.ktor:ktor-server-content-negotiation")
implementation("io.ktor:ktor-serialization-kotlinx-json")
implementation("io.ktor:ktor-server-call-logging")
implementation("io.ktor:ktor-server-status-pages")
testImplementation("io.ktor:ktor-server-test-host")
testImplementation("org.jetbrains.kotlin:kotlin-test-junit")
}
Simple explanation:
1
2
3
4
5
6
7
ktor-server-core Main Ktor server features
ktor-server-netty Server engine
content-negotiation JSON conversion plugin
kotlinx-json JSON serialization library
call-logging API request logging
status-pages Error handling
ktor-server-test-host Testing support
6. application.conf
application.conf stores server configuration.
Example:
1
2
3
4
5
6
7
8
9
10
ktor {
deployment {
port = 8080
host = "0.0.0.0"
}
application {
modules = [ com.example.ApplicationKt.module ]
}
}
Simple explanation:
1
2
3
port = 8080 Server will run on port 8080
host = 0.0.0.0 Server accepts external connections
modules Tells Ktor which function starts the app
7. Main Application File
Application.kt is the main backend entry point.
1
2
3
4
5
6
7
8
9
10
package com.example
import io.ktor.server.application.*
fun Application.module() {
configureSerialization()
configureMonitoring()
configureStatusPages()
configureRouting()
}
Simple explanation:
1
2
3
4
5
6
7
module() is the starting function of the Ktor app.
Inside this function, we install and configure different features:
- JSON
- Logging
- Error handling
- Routes
8. Basic Hello World API
This is the simplest Ktor API.
1
2
3
4
5
6
7
8
9
10
11
12
13
package com.example
import io.ktor.server.application.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
fun Application.configureRouting() {
routing {
get("/") {
call.respondText("Hello Ktor Backend")
}
}
}
When you open:
1
http://localhost:8080/
Response:
1
Hello Ktor Backend
Simple explanation:
1
2
3
routing { } Defines API routes
get("/") Handles GET request on root URL
call.respondText Sends text response
9. Routing
Routing means defining API endpoints.
Example:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
fun Application.configureRouting() {
routing {
get("/") {
call.respondText("Home API")
}
get("/health") {
call.respondText("Server is running")
}
get("/about") {
call.respondText("This is a Ktor backend")
}
}
}
API endpoints:
1
2
3
GET / Home API
GET /health Server is running
GET /about This is a Ktor backend
10. Route Grouping
For clean code, group related routes.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
fun Route.userRoutes() {
route("/users") {
get {
call.respondText("Get all users")
}
get("/{id}") {
val id = call.parameters["id"]
call.respondText("Get user with ID: $id")
}
post {
call.respondText("Create user")
}
}
}
Use it inside main routing:
1
2
3
4
5
fun Application.configureRouting() {
routing {
userRoutes()
}
}
Simple explanation:
1
2
3
/users Get all users
/users/1 Get user ID 1
POST /users Create user
11. Path Parameters
Path parameters come from the URL path.
Example URL:
1
GET /users/10
Ktor code:
1
2
3
4
5
get("/users/{id}") {
val id = call.parameters["id"]
call.respondText("User ID is $id")
}
If request is:
1
/users/10
Response:
1
User ID is 10
12. Query Parameters
Query parameters come after ? in the URL.
Example URL:
1
GET /products?page=1&limit=20
Ktor code:
1
2
3
4
5
6
get("/products") {
val page = call.request.queryParameters["page"] ?: "1"
val limit = call.request.queryParameters["limit"] ?: "10"
call.respondText("Page: $page, Limit: $limit")
}
Simple explanation:
1
2
page and limit are optional.
If not provided, default values are used.
13. JSON Serialization
To send and receive JSON, install ContentNegotiation.
1
2
3
4
5
6
7
8
9
import io.ktor.serialization.kotlinx.json.*
import io.ktor.server.application.*
import io.ktor.server.plugins.contentnegotiation.*
fun Application.configureSerialization() {
install(ContentNegotiation) {
json()
}
}
Simple explanation:
1
2
ContentNegotiation converts Kotlin objects to JSON
and JSON to Kotlin objects.
14. Response DTO
DTO means Data Transfer Object.
It is used for API request and response data.
1
2
3
4
5
6
7
8
import kotlinx.serialization.Serializable
@Serializable
data class UserResponse(
val id: Int,
val name: String,
val email: String
)
Send JSON response:
1
2
3
4
5
6
7
8
9
get("/user") {
val user = UserResponse(
id = 1,
name = "Envobyte",
email = "test@example.com"
)
call.respond(user)
}
Response:
1
2
3
4
5
{
"id": 1,
"name": "Envobyte",
"email": "test@example.com"
}
Important:
1
@Serializable is required for kotlinx JSON serialization.
15. POST Request Body
To receive JSON from client, use call.receive().
Request model:
1
2
3
4
5
6
7
import kotlinx.serialization.Serializable
@Serializable
data class CreateUserRequest(
val name: String,
val email: String
)
Route:
1
2
3
4
5
post("/users") {
val request = call.receive<CreateUserRequest>()
call.respondText("User created: ${request.name}")
}
Client sends:
1
2
3
4
{
"name": "Envobyte",
"email": "test@example.com"
}
Server response:
1
User created: Envobyte
Simple explanation:
1
2
call.receive<CreateUserRequest>() reads JSON body
and converts it into Kotlin object.
16. HTTP Status Code Response
You can return proper status codes.
1
2
3
4
5
6
7
8
9
10
11
12
13
import io.ktor.http.*
post("/users") {
val request = call.receive<CreateUserRequest>()
val response = UserResponse(
id = 1,
name = request.name,
email = request.email
)
call.respond(HttpStatusCode.Created, response)
}
Simple explanation:
1
201 Created means new data was successfully created.
17. Error Response Model
Always return clean error responses.
1
2
3
4
5
6
7
import kotlinx.serialization.Serializable
@Serializable
data class ErrorResponse(
val success: Boolean = false,
val message: String
)
Example:
1
2
3
4
5
6
7
8
9
10
11
12
13
get("/users/{id}") {
val id = call.parameters["id"]?.toIntOrNull()
if (id == null) {
call.respond(
HttpStatusCode.BadRequest,
ErrorResponse(message = "Invalid user ID")
)
return@get
}
call.respondText("User ID: $id")
}
Simple explanation:
1
If ID is not a valid number, server returns 400 Bad Request.
18. StatusPages Error Handling
StatusPages handles errors globally.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.plugins.statuspages.*
import io.ktor.server.response.*
fun Application.configureStatusPages() {
install(StatusPages) {
exception<IllegalArgumentException> { call, cause ->
call.respond(
HttpStatusCode.BadRequest,
ErrorResponse(message = cause.message ?: "Bad request")
)
}
exception<Throwable> { call, cause ->
call.respond(
HttpStatusCode.InternalServerError,
ErrorResponse(message = "Internal server error")
)
}
status(HttpStatusCode.NotFound) { call, status ->
call.respond(
status,
ErrorResponse(message = "Route not found")
)
}
}
}
Simple explanation:
1
2
Instead of writing try-catch in every route,
StatusPages catches errors globally.
19. Validation
Validation means checking if user input is correct.
Example:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
post("/register") {
val request = call.receive<CreateUserRequest>()
if (request.name.isBlank()) {
call.respond(
HttpStatusCode.BadRequest,
ErrorResponse(message = "Name is required")
)
return@post
}
if (!request.email.contains("@")) {
call.respond(
HttpStatusCode.BadRequest,
ErrorResponse(message = "Invalid email address")
)
return@post
}
call.respondText("Registration successful")
}
Simple explanation:
1
2
Never trust client input.
Always validate request body on backend.
20. Repository Pattern
Repository handles data operations.
For learning, start with in-memory data before database.
Model:
1
2
3
4
5
6
7
8
import kotlinx.serialization.Serializable
@Serializable
data class User(
val id: Int,
val name: String,
val email: String
)
Repository:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class UserRepository {
private val users = mutableListOf<User>()
fun getAllUsers(): List<User> {
return users
}
fun getUserById(id: Int): User? {
return users.find { it.id == id }
}
fun createUser(name: String, email: String): User {
val user = User(
id = users.size + 1,
name = name,
email = email
)
users.add(user)
return user
}
fun deleteUser(id: Int): Boolean {
return users.removeIf { it.id == id }
}
}
Simple explanation:
1
2
Repository separates data logic from route logic.
This makes code cleaner and easier to test.
21. Service Layer
Service layer contains business logic.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class UserService(
private val repository: UserRepository
) {
fun getUsers(): List<User> {
return repository.getAllUsers()
}
fun createUser(request: CreateUserRequest): User {
if (request.name.isBlank()) {
throw IllegalArgumentException("Name is required")
}
if (!request.email.contains("@")) {
throw IllegalArgumentException("Invalid email")
}
return repository.createUser(
name = request.name,
email = request.email
)
}
}
Simple explanation:
1
2
3
Route receives request.
Service handles rules.
Repository handles data.
Flow:
1
Route → Service → Repository → Data
22. Route with Service and Repository
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
fun Route.userRoutes(userService: UserService) {
route("/users") {
get {
val users = userService.getUsers()
call.respond(users)
}
post {
val request = call.receive<CreateUserRequest>()
val user = userService.createUser(request)
call.respond(HttpStatusCode.Created, user)
}
}
}
Main setup:
1
2
3
4
5
6
7
8
fun Application.configureRouting() {
val userRepository = UserRepository()
val userService = UserService(userRepository)
routing {
userRoutes(userService)
}
}
Simple explanation:
1
2
This is manual dependency injection.
We create Repository, then Service, then pass Service into route.
23. Authentication Basics
Authentication means checking who the user is.
Common authentication methods:
1
2
3
4
5
Email/password login
JWT token
Session
API key
OAuth login
For mobile app backend, JWT is very common.
Flow:
1
2
3
4
5
6
7
8
9
10
11
User logs in
↓
Backend validates email/password
↓
Backend returns JWT token
↓
App stores token
↓
App sends token in Authorization header
↓
Backend allows protected routes
Header example:
1
Authorization: Bearer jwt-token-here
24. Simple Login Route
Request:
1
2
3
4
5
@Serializable
data class LoginRequest(
val email: String,
val password: String
)
Response:
1
2
3
4
@Serializable
data class LoginResponse(
val token: String
)
Route:
1
2
3
4
5
6
7
8
9
10
11
12
post("/login") {
val request = call.receive<LoginRequest>()
if (request.email == "admin@example.com" && request.password == "123456") {
call.respond(LoginResponse(token = "fake-jwt-token"))
} else {
call.respond(
HttpStatusCode.Unauthorized,
ErrorResponse(message = "Invalid email or password")
)
}
}
Simple explanation:
1
2
This is only for learning.
In real apps, password must come from database and should be hashed.
25. JWT Authentication Concept
JWT means JSON Web Token.
Simple JWT flow:
1
2
3
Login success → generate token → send token to client
Client saves token → sends token with every protected request
Backend verifies token → gives access
Protected API example:
1
2
GET /profile
Authorization: Bearer token-here
26. Password Hashing
Never store raw passwords.
Bad:
1
password = 123456
Good:
1
password_hash = hashed_password_value
Simple hashing example:
1
2
3
4
5
6
7
8
import java.security.MessageDigest
fun sha256(text: String): String {
val bytes = MessageDigest.getInstance("SHA-256")
.digest(text.toByteArray())
return bytes.joinToString("") { "%02x".format(it) }
}
Important:
1
2
For real production apps, use strong password hashing like BCrypt or Argon2.
Do not use plain SHA-256 alone for real password storage.
27. Authorization
Authentication checks:
1
Who are you?
Authorization checks:
1
What are you allowed to do?
Example:
1
2
3
4
5
6
7
8
9
10
11
12
13
get("/admin") {
val userRole = "USER"
if (userRole != "ADMIN") {
call.respond(
HttpStatusCode.Forbidden,
ErrorResponse(message = "You do not have permission")
)
return@get
}
call.respondText("Welcome admin")
}
Simple explanation:
1
A logged-in user may not be allowed to access every route.
28. Database Basics
Most backend apps need a database.
Common databases:
1
2
3
4
5
6
PostgreSQL
MySQL
MariaDB
SQLite
MongoDB
Redis
For Ktor with relational database, common options:
1
2
3
4
5
6
Exposed
Ktorm
JDBC
Jooq
SQLDelight
Hibernate
Start simple with:
1
PostgreSQL + Exposed
29. Database Table Concept
A table stores data in rows and columns.
Example users table:
1
2
3
4
id | name | email
---|----------|------------------
1 | Envobyte | test@example.com
2 | John | john@example.com
SQL example:
1
2
3
4
5
CREATE TABLE users (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
email VARCHAR(255) NOT NULL UNIQUE
);
30. Exposed Table Example
Exposed is a Kotlin SQL library.
1
2
3
4
5
6
7
8
9
import org.jetbrains.exposed.sql.Table
object UsersTable : Table("users") {
val id = integer("id").autoIncrement()
val name = varchar("name", 255)
val email = varchar("email", 255).uniqueIndex()
override val primaryKey = PrimaryKey(id)
}
Simple explanation:
1
This Kotlin object represents the users table in database.
31. Database Connection Example
1
2
3
4
5
6
7
8
9
10
11
12
import org.jetbrains.exposed.sql.Database
object DatabaseFactory {
fun init() {
Database.connect(
url = "jdbc:postgresql://localhost:5432/my_database",
driver = "org.postgresql.Driver",
user = "postgres",
password = "password"
)
}
}
Use it in Application.kt:
1
2
3
4
5
6
7
fun Application.module() {
DatabaseFactory.init()
configureSerialization()
configureStatusPages()
configureRouting()
}
Simple explanation:
1
DatabaseFactory.init() connects Ktor backend to database.
32. Database Transaction
Database operations should run inside transactions.
1
2
3
4
5
6
7
8
9
10
11
12
13
import org.jetbrains.exposed.sql.transactions.transaction
fun getUsersFromDatabase(): List<User> {
return transaction {
UsersTable.selectAll().map {
User(
id = it[UsersTable.id],
name = it[UsersTable.name],
email = it[UsersTable.email]
)
}
}
}
Simple explanation:
1
transaction { } safely runs database queries.
33. Environment Variables
Never hardcode secrets in code.
Bad:
1
val dbPassword = "my-secret-password"
Better:
1
val dbPassword = System.getenv("DB_PASSWORD")
Example:
1
2
3
4
5
6
object AppConfig {
val databaseUrl: String = System.getenv("DB_URL") ?: "jdbc:postgresql://localhost:5432/app"
val databaseUser: String = System.getenv("DB_USER") ?: "postgres"
val databasePassword: String = System.getenv("DB_PASSWORD") ?: ""
val jwtSecret: String = System.getenv("JWT_SECRET") ?: "dev-secret"
}
Simple explanation:
1
Environment variables keep secrets outside source code.
34. CORS
CORS controls which websites can call your backend.
Useful when frontend website calls your API.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.plugins.cors.routing.*
fun Application.configureCors() {
install(CORS) {
allowHost("localhost:3000")
allowHeader(HttpHeaders.ContentType)
allowHeader(HttpHeaders.Authorization)
allowMethod(HttpMethod.Get)
allowMethod(HttpMethod.Post)
allowMethod(HttpMethod.Put)
allowMethod(HttpMethod.Delete)
}
}
Simple explanation:
1
2
CORS is mainly important for browser-based clients.
Android/iOS apps usually do not need CORS like browsers do.
35. Logging
Logging helps you understand what is happening in your backend.
1
2
3
4
5
6
import io.ktor.server.application.*
import io.ktor.server.plugins.calllogging.*
fun Application.configureMonitoring() {
install(CallLogging)
}
Simple explanation:
1
2
CallLogging logs incoming API requests.
Useful for debugging and monitoring.
36. Custom Logger
1
2
3
4
5
6
7
8
9
object AppLogger {
fun info(message: String) {
println("INFO: $message")
}
fun error(message: String) {
println("ERROR: $message")
}
}
Use:
1
2
AppLogger.info("User login started")
AppLogger.error("Failed to connect database")
Production note:
1
For real production, use proper logging with Logback.
37. File Upload
File upload is common for image, PDF, audio, and video apps.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import io.ktor.http.content.*
import io.ktor.server.request.*
import java.io.File
post("/upload") {
val multipart = call.receiveMultipart()
multipart.forEachPart { part ->
if (part is PartData.FileItem) {
val fileName = part.originalFileName ?: "uploaded-file"
val file = File("uploads/$fileName")
part.streamProvider().use { input ->
file.outputStream().buffered().use { output ->
input.copyTo(output)
}
}
}
part.dispose()
}
call.respondText("File uploaded successfully")
}
Simple explanation:
1
2
3
receiveMultipart() reads file upload request.
FileItem means uploaded file.
copyTo() saves the uploaded file.
Important:
1
2
In production, validate file type, size, extension, and storage path.
Never blindly trust uploaded files.
38. File Download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import java.io.File
get("/download/{fileName}") {
val fileName = call.parameters["fileName"] ?: return@get call.respond(
HttpStatusCode.BadRequest,
ErrorResponse(message = "File name is required")
)
val file = File("uploads/$fileName")
if (!file.exists()) {
call.respond(
HttpStatusCode.NotFound,
ErrorResponse(message = "File not found")
)
return@get
}
call.respondFile(file)
}
Simple explanation:
1
respondFile() sends a file as response.
Security note:
1
2
Do not allow unsafe paths like ../../secret.txt.
Use safe file validation.
39. Pagination
Pagination means loading data page by page.
Example URL:
1
GET /products?page=1&limit=20
Code:
1
2
3
4
5
6
7
8
9
10
11
12
get("/products") {
val page = call.request.queryParameters["page"]?.toIntOrNull() ?: 1
val limit = call.request.queryParameters["limit"]?.toIntOrNull() ?: 20
val allProducts = listOf("Product 1", "Product 2", "Product 3", "Product 4")
val result = allProducts
.drop((page - 1) * limit)
.take(limit)
call.respond(result)
}
Simple explanation:
1
2
page tells which page user wants.
limit tells how many items per page.
40. API Versioning
API versioning helps you update APIs without breaking old apps.
Example:
1
2
/api/v1/users
/api/v2/users
Route:
1
2
3
4
5
6
7
8
9
10
11
12
13
routing {
route("/api/v1") {
get("/users") {
call.respondText("Version 1 users API")
}
}
route("/api/v2") {
get("/users") {
call.respondText("Version 2 users API")
}
}
}
Simple explanation:
1
2
Old app can keep using v1.
New app can use v2.
41. WebSocket
WebSocket is used for real-time communication.
Use cases:
- Chat app
- Live notification
- Live tracking
- Live dashboard
- Game server
- Real-time order progress
Example:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import io.ktor.server.websocket.*
import io.ktor.websocket.*
import java.time.Duration
fun Application.configureWebSockets() {
install(WebSockets) {
pingPeriod = Duration.ofSeconds(15)
timeout = Duration.ofSeconds(30)
}
routing {
webSocket("/chat") {
send("Connected to chat server")
for (frame in incoming) {
if (frame is Frame.Text) {
val text = frame.readText()
send("Server received: $text")
}
}
}
}
}
Simple explanation:
1
2
HTTP request gives one response.
WebSocket keeps connection open for real-time messages.
42. Background Jobs
Sometimes backend needs background work.
Examples:
- Send email
- Clean old files
- Process video
- Generate report
- Update order status
- Sync third-party API
Simple coroutine example:
1
2
3
4
5
6
7
8
9
10
import kotlinx.coroutines.*
fun startBackgroundJob() {
CoroutineScope(Dispatchers.Default).launch {
while (true) {
println("Running background job")
delay(60_000)
}
}
}
Simple explanation:
1
This runs a task every 60 seconds.
Production note:
1
2
For serious jobs, use a proper scheduler, queue, or worker system.
Examples: Quartz, Redis queue, RabbitMQ, Kafka, Cloud Tasks.
43. Ktor HTTP Client
Ktor can also call other APIs using Ktor Client.
Example use cases:
- Call payment gateway API
- Call AI API
- Call Google API
- Call third-party service
1
2
3
4
5
6
7
8
9
10
import io.ktor.client.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
val client = HttpClient()
suspend fun fetchExternalData(): String {
val response: HttpResponse = client.get("https://api.example.com/data")
return response.bodyAsText()
}
Simple explanation:
1
2
Ktor Server receives request from your app.
Ktor Client can send request to another external API.
44. Dependency Injection
Dependency Injection means passing dependencies from outside.
Manual example:
1
2
3
4
5
6
val userRepository = UserRepository()
val userService = UserService(userRepository)
routing {
userRoutes(userService)
}
Simple explanation:
1
2
3
UserService needs UserRepository.
Instead of creating repository inside service,
we pass it from outside.
For bigger projects, use:
1
2
3
4
5
Koin
Kodein
Dagger
Ktor built-in DI
Manual DI
45. Clean Architecture for Ktor
A clean backend separates responsibilities.
Recommended structure:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
src/main/kotlin/com/example/
│
├── Application.kt
├── plugins/
│ ├── Routing.kt
│ ├── Serialization.kt
│ ├── Security.kt
│ ├── Monitoring.kt
│ └── StatusPages.kt
│
├── features/
│ ├── auth/
│ │ ├── AuthRoutes.kt
│ │ ├── AuthService.kt
│ │ ├── AuthRepository.kt
│ │ ├── LoginRequest.kt
│ │ └── LoginResponse.kt
│ │
│ └── user/
│ ├── UserRoutes.kt
│ ├── UserService.kt
│ ├── UserRepository.kt
│ ├── User.kt
│ └── UserResponse.kt
│
├── database/
│ ├── DatabaseFactory.kt
│ └── tables/
│ └── UsersTable.kt
│
├── common/
│ ├── ErrorResponse.kt
│ ├── ApiResponse.kt
│ └── AppConfig.kt
│
└── security/
├── JwtConfig.kt
└── PasswordHasher.kt
Simple explanation:
1
2
3
4
5
plugins/ Ktor setup
features/ Feature-based business code
database/ Database setup and tables
common/ Shared classes
security/ Auth/security logic
46. API Response Wrapper
Many backends use a common response format.
1
2
3
4
5
6
7
8
import kotlinx.serialization.Serializable
@Serializable
data class ApiResponse<T>(
val success: Boolean,
val message: String,
val data: T? = null
)
Example:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
get("/profile") {
val user = User(
id = 1,
name = "Envobyte",
email = "test@example.com"
)
call.respond(
ApiResponse(
success = true,
message = "Profile loaded successfully",
data = user
)
)
}
Response:
1
2
3
4
5
6
7
8
9
{
"success": true,
"message": "Profile loaded successfully",
"data": {
"id": 1,
"name": "Envobyte",
"email": "test@example.com"
}
}
47. Testing Ktor APIs
Ktor supports testing without running a real server.
Example:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.server.testing.*
import kotlin.test.Test
import kotlin.test.assertEquals
class ApplicationTest {
@Test
fun testRootRoute() = testApplication {
application {
module()
}
val response = client.get("/")
assertEquals("Hello Ktor Backend", response.bodyAsText())
}
}
Simple explanation:
1
2
3
testApplication starts a test version of your Ktor app.
client.get("/") sends fake test request.
No real server port is needed.
48. Testing JSON API
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import io.ktor.client.call.*
import io.ktor.client.request.*
import io.ktor.http.*
import io.ktor.server.testing.*
import kotlin.test.Test
import kotlin.test.assertEquals
class UserRouteTest {
@Test
fun createUserTest() = testApplication {
application {
module()
}
val response = client.post("/users") {
contentType(ContentType.Application.Json)
setBody(
"""
{
"name": "Envobyte",
"email": "test@example.com"
}
""".trimIndent()
)
}
assertEquals(HttpStatusCode.Created, response.status)
}
}
Simple explanation:
1
2
This test sends a POST request with JSON body
and checks if status code is 201 Created.
49. Security Checklist
For production Ktor backend, remember:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Use HTTPS
Validate all input
Hash passwords
Use JWT/session safely
Never hardcode secrets
Use environment variables
Protect admin routes
Add rate limiting
Check request body size
Sanitize file uploads
Use proper database permissions
Use CORS carefully
Log errors without exposing secrets
Do not return stack traces to users
Use server-side validation
Use token expiry
Use refresh token carefully
Add replay protection for sensitive APIs
Use nonce/timestamp for high-risk operations
50. Rate Limiting
Rate limiting protects APIs from abuse.
Example use cases:
1
2
3
4
5
Limit login attempts
Limit OTP requests
Limit free order creation
Limit file uploads
Limit AI generation requests
Simple concept:
1
2
User can call /login only 5 times per minute.
After that, return 429 Too Many Requests.
Example response:
1
2
3
4
call.respond(
HttpStatusCode.TooManyRequests,
ErrorResponse(message = "Too many requests. Try again later.")
)
51. Request Signing Concept
For high-security mobile apps, request signing can help protect APIs from scripted abuse.
Basic idea:
1
2
3
4
5
6
Client creates request body
Client adds timestamp
Client adds nonce
Client signs payload
Server verifies signature
Server rejects replayed request
Payload example:
1
2
3
4
5
6
7
8
9
10
import kotlinx.serialization.Serializable
@Serializable
data class SignedRequest(
val path: String,
val timestamp: Long,
val nonce: String,
val body: String,
val signature: String
)
Simple explanation:
1
2
3
timestamp prevents old request reuse.
nonce prevents duplicate request replay.
signature proves request was created by trusted client logic.
Important:
1
2
Do not rely only on app-side security.
Always validate everything on the server.
52. Deployment Options
You can deploy Ktor backend using:
1
2
3
4
5
6
7
8
9
10
VPS
Docker
Google Cloud Run
AWS
Azure
Railway
Render
Heroku alternatives
Kubernetes
Linux server with systemd
Simple deployment flow:
1
2
3
4
5
6
Build JAR
Upload to server
Run with Java
Put Nginx in front
Add HTTPS
Monitor logs
53. Build Runnable JAR
Command:
1
./gradlew build
Run:
1
java -jar build/libs/my-app-all.jar
Simple explanation:
1
2
The JAR file contains your backend app.
You can run it on a server with Java installed.
54. Dockerfile
Docker packages your backend with its runtime.
1
2
3
4
5
6
7
8
9
FROM eclipse-temurin:21-jre
WORKDIR /app
COPY build/libs/my-app-all.jar app.jar
EXPOSE 8080
CMD ["java", "-jar", "app.jar"]
Simple explanation:
1
2
Docker creates a container for your Ktor backend.
It makes deployment more consistent.
55. Nginx Reverse Proxy
Nginx can sit in front of Ktor.
Flow:
1
User → Nginx → Ktor Backend
Why use Nginx?
1
2
3
4
5
6
HTTPS
Domain setup
Reverse proxy
Load balancing
Static file serving
Security headers
Example:
1
2
3
4
5
6
7
8
9
10
server {
listen 80;
server_name api.example.com;
location / {
proxy_pass http://localhost:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
Simple explanation:
1
2
User calls api.example.com.
Nginx forwards request to Ktor running on localhost:8080.
56. Monitoring
Monitoring helps you know if your backend is healthy.
Monitor:
1
2
3
4
5
6
7
8
9
10
Server uptime
API response time
Error count
CPU usage
RAM usage
Database health
Disk usage
Request count
Failed login attempts
Slow queries
Simple health route:
1
2
3
4
5
6
7
8
get("/health") {
call.respond(
mapOf(
"status" to "OK",
"message" to "Server is running"
)
)
}
57. Common Ktor Plugins
Useful Ktor plugins:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
Routing API routes
ContentNegotiation JSON/XML serialization
StatusPages Error handling
CallLogging Request logging
Authentication Login/JWT/session auth
CORS Browser frontend access control
Sessions Cookie/session management
Compression Compress response
DefaultHeaders Add default headers
WebSockets Real-time connection
RateLimit Limit repeated requests
RequestValidation Validate incoming requests
PartialContent File/video streaming support
AutoHeadResponse Auto HEAD response
58. Common Backend Mistakes
Avoid these mistakes:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Putting all code in Application.kt
No validation
No error handling
Hardcoded secrets
Raw password storage
Returning stack traces to users
No database indexing
No pagination
No logging
No tests
No environment separation
No authentication on private routes
No rate limiting
No HTTPS
No backup plan
No monitoring
No clean project structure
59. Recommended Learning Order
Follow this order:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
1. Kotlin basics
2. HTTP basics
3. REST API concept
4. Ktor project setup
5. Routing
6. GET and POST APIs
7. JSON serialization
8. Request and response DTO
9. Status codes
10. Error handling
11. Validation
12. Repository pattern
13. Service layer
14. Authentication basics
15. JWT concept
16. Database basics
17. PostgreSQL or MySQL
18. Exposed ORM
19. Environment variables
20. CORS
21. Logging
22. File upload/download
23. Pagination
24. API versioning
25. WebSocket
26. Background jobs
27. Ktor Client
28. Dependency Injection
29. Clean Architecture
30. Testing
31. Security
32. Rate limiting
33. Request signing
34. Docker
35. Nginx
36. Deployment
37. Monitoring
60. Mini Project: Simple User API
Build this project to practice Ktor.
Features:
1
2
3
4
5
6
GET /health
GET /users
GET /users/{id}
POST /users
POST /login
DELETE /users/{id}
Data model:
1
2
3
4
5
6
@Serializable
data class User(
val id: Int,
val name: String,
val email: String
)
Request model:
1
2
3
4
5
@Serializable
data class CreateUserRequest(
val name: String,
val email: String
)
Repository:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class UserRepository {
private val users = mutableListOf<User>()
fun getAll(): List<User> {
return users
}
fun findById(id: Int): User? {
return users.find { it.id == id }
}
fun create(request: CreateUserRequest): User {
val user = User(
id = users.size + 1,
name = request.name,
email = request.email
)
users.add(user)
return user
}
fun delete(id: Int): Boolean {
return users.removeIf { it.id == id }
}
}
Routes:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
fun Route.userRoutes(repository: UserRepository) {
route("/users") {
get {
call.respond(repository.getAll())
}
get("/{id}") {
val id = call.parameters["id"]?.toIntOrNull()
if (id == null) {
call.respond(
HttpStatusCode.BadRequest,
ErrorResponse(message = "Invalid user ID")
)
return@get
}
val user = repository.findById(id)
if (user == null) {
call.respond(
HttpStatusCode.NotFound,
ErrorResponse(message = "User not found")
)
return@get
}
call.respond(user)
}
post {
val request = call.receive<CreateUserRequest>()
if (request.name.isBlank() || !request.email.contains("@")) {
call.respond(
HttpStatusCode.BadRequest,
ErrorResponse(message = "Invalid user data")
)
return@post
}
val user = repository.create(request)
call.respond(HttpStatusCode.Created, user)
}
delete("/{id}") {
val id = call.parameters["id"]?.toIntOrNull()
if (id == null) {
call.respond(
HttpStatusCode.BadRequest,
ErrorResponse(message = "Invalid user ID")
)
return@delete
}
val deleted = repository.delete(id)
if (!deleted) {
call.respond(
HttpStatusCode.NotFound,
ErrorResponse(message = "User not found")
)
return@delete
}
call.respondText("User deleted successfully")
}
}
}
Main routing:
1
2
3
4
5
6
7
8
9
10
11
fun Application.configureRouting() {
val userRepository = UserRepository()
routing {
get("/health") {
call.respondText("Server is running")
}
userRoutes(userRepository)
}
}
Final Note
Ktor backend development is mainly about understanding:
1
2
3
4
5
6
7
8
9
10
11
12
13
Request
Response
Route
DTO
Service
Repository
Database
Authentication
Validation
Error handling
Security
Testing
Deployment
Simple backend flow:
1
2
3
4
5
6
7
8
9
10
11
12
13
Client App
↓
Ktor Route
↓
Validation
↓
Service Layer
↓
Repository Layer
↓
Database
↓
Response to Client