Skip to Content
Course content

190: Building a Simple Note-Taking App with Ktor Backend

Click on the "Edit" button in the top corner of the screen to edit your slide content.

How should I set up my Note model for JSON?

When you're building a Ktor backend, the first thing I always suggest is getting your data models sorted. Since we're making a note-taking app, we need a Note class. But here's the catch: Ktor doesn't magically know how to turn a Kotlin object into JSON. You need the kotlinx.serialization library for that.

I usually keep it simple. You'll want a unique ID, a title, and the actual body of the note. I've added the @Serializable annotation here—without this, Ktor will throw an error the moment you try to respond with a note object.

import kotlinx.serialization.Serializable

@Serializable
data class Note(
    val id: Int,
    val title: String,
    val content: String
)

One pro tip: if you're planning to let users update notes later, you might want to make the fields optional or use a separate "Request" class, but for this simple version, a standard data class is perfect.

Where do I put the logic to store and retrieve the notes?

You could technically throw your notes into a list right inside the routing block, but that's a recipe for messy code. I prefer creating a separate "Repository" class. It keeps the data logic separate from the HTTP logic, which makes your life much easier when you eventually decide to move from an in-memory list to a real database like PostgreSQL or MongoDB.

Since Ktor is asynchronous, be careful with a standard ArrayList. I'll use a synchronized list here to prevent the app from crashing if two people try to save a note at the exact same millisecond.

import java.util.Collections

class NoteRepository {
    private val notes = Collections.synchronizedList(mutableListOf<Note>())
    private var currentId = 1

    fun getAllNotes(): List<Note> = notes

    fun addNote(title: String, content: String): Note {
        val newNote = Note(currentId++, title, content)
        notes.add(newNote)
        return newNote
    }
}

What does the routing code look like for the API endpoints?

Now we tie it all together. In your Application.module, you'll define your routes. You need a GET endpoint to fetch all notes and a POST endpoint to create a new one. I find it helpful to instantiate the repository outside the routing block so the state persists as long as the server is running.

Notice how we use call.receive<Note>(). This is where Ktor's ContentNegotiation plugin does the heavy lifting, converting the incoming JSON body directly into our Kotlin object.

routing {
    val repository = NoteRepository()

    route("/notes") {
        get {
            val allNotes = repository.getAllNotes()
            call.respond(allNotes)
        }
        
        post {
            val noteRequest = call.receive<Note>()
            val createdNote = repository.addNote(noteRequest.title, noteRequest.content)
            call.respond(HttpStatusCode.Created, createdNote)
        }
    }
}

I used HttpStatusCode.Created (201) for the POST request because it's more descriptive than a generic 200 OK. It tells the client, "Yes, I actually created a new resource for you."




📋 Practical Task

Implement a Note Deletion Endpoint

Now that you have the basic "Read" and "Create" functionality working, it's time to handle deletions. Your task is to extend the Note-Taking app by adding a DELETE endpoint.

  • Modify the NoteRepository class to include a deleteNote(id: Int): Boolean method that removes a note by its ID and returns true if it was found and removed, or false otherwise.
  • Add a new route delete("/{id}") inside the /notes block.
  • The route should extract the id from the path parameters, call the repository, and respond with HttpStatusCode.NoContent (204) if the deletion was successful, or HttpStatusCode.NotFound (404) if the note didn't exist.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.