プログラム 技術

KotlinでAPIサーバーを実装する

前回はKotlinで非同期処理について記載しましたが、今回はKotlinとSpring BootでAPIサーバーを実装してみます
実装内容としてはよく使用されるToDoリストです

前回の記事
Kotlinで非同期処理を実装する - ナストンのまとめ
Kotlinで非同期処理を実装する - ナストンのまとめ

今回はKotlinでの非同期処理についてになります。非同期処理は【kotlinx-coroutines-core】モジュ ...

前回の記事へ

名称バージョン
jdk21
Kotlin2.1.20
org.springframework.boot3.2.0
io.spring.dependency-management1.1.0

Gradleに追加する

Spring Bootを使用するために『build.gradle.kts』ファイルに対して以下の内容を追加し、Gradleを更新します

plugins {
    kotlin("jvm") version "2.1.20"
    kotlin("plugin.spring") version "2.1.20"                 // 追加
    id("org.springframework.boot") version "3.2.0"           // 追加
    id("io.spring.dependency-management") version "1.1.0"    // 追加
}

dependencies {
    implementation("org.springframework.boot:spring-boot-starter-web")       // 追加
    implementation("com.fasterxml.jackson.module:jackson-module-kotlin")     // 追加
    implementation("org.jetbrains.kotlin:kotlin-reflect")                    // 追加

    testImplementation("org.springframework.boot:spring-boot-starter-test")  // 追加
    testImplementation(kotlin("test"))
}

各機能の実装

これでSpring Bootが使用できるようになったので実際にAPIサーバーとしての機能を実装していきます

モデルの作成

識別子であるidとタイトル、完了フラグの3つだけのものを作成します

data class Todo(
    val id: Long,
    val title: String,
    val completed: Boolean = false
)

コントローラーの作成

続いて、APIリクエストを受け取るための窓口を作成します

// Todo 作成リクエストの DTO
data class CreateTodoRequest(val title: String)

 // Todo 更新(PATCH)用の DTO
data class UpdateTodoRequest(val title: String?, val completed: Boolean?)

/**
 * Todo 用の REST コントローラ
 * - /api/todos に対する CRUD の入口
 */
@RestController
@RequestMapping("/api/todos")
class TodoController(private val service: TodoService) {

    /**
     * すべての Todo を取得して返すエンドポイント
     * 成功時は 200 OK と Todo の配列を返す
     */
    @GetMapping
    fun all(): List<Todo> = service.findAll()

    /**
     * 指定 ID の Todo を取得するエンドポイント
     * - 存在する場合 200 OK と Todo を返す
     * - 存在しない場合は 404 Not Found を返す
     */
    @GetMapping("/{id}")
    fun get(@PathVariable id: Long): ResponseEntity<Todo> {
        val todo = service.findById(id) ?: return ResponseEntity.notFound().build()
        return ResponseEntity.ok(todo)
    }

    /**
     * Todo を作成するエンドポイント
     * - リクエストボディでタイトルを受け取り、新規 Todo を作成して返す
     * - 作成成功時は 201 Created を返す
     */
    @PostMapping
    fun create(@RequestBody req: CreateTodoRequest): ResponseEntity<Todo> {
        val created = service.create(req.title)
        return ResponseEntity.status(201).body(created)
    }

    /**
     * 指定 ID の Todo を更新する(PATCH)
     * - 部分更新を想定し、リクエストボディで変更したいフィールドのみ渡す
     * - 指定 ID が存在しない場合は 404 Not Found を返す
     * - 更新成功時は 200 OK と更新済みの Todo を返す
     */
    @PatchMapping("/{id}")
    fun patch(@PathVariable id: Long, @RequestBody req: UpdateTodoRequest): ResponseEntity<Todo> {
        val newTitle = req.title ?: return ResponseEntity.badRequest().build()
        var newcompleted = req.completed ?: return ResponseEntity.badRequest().build()
        val updated = service.update(id, newTitle, newcompleted) ?: return ResponseEntity.notFound().build()
        return ResponseEntity.ok(updated)
    }

    /**
     * 指定 ID の Todo を削除する
     * - 削除成功時は 204 No Content を返す
     * - 指定した ID が存在しない場合は 404 Not Found を返す
     */
    @DeleteMapping("/{id}")
    fun delete(@PathVariable id: Long): ResponseEntity<Void> {
        val deleted = service.delete(id)
        return if (deleted) ResponseEntity.noContent().build() else ResponseEntity.notFound().build()
    }
}

サービスの作成

コントローラーとリポジトリ(実際の処理を実施)するを繋ぐ仲介処理となります。APIで受け取った値などを判定し、その値を実処理へと渡す形です

/**
 * Todo の業務ロジックを扱うサービス層。
 * コントローラから呼び出され、リポジトリへの永続化操作を仲介
 */
@Service
class TodoService(private val repository: TodoRepository) {

    /**
     * すべての Todo を取得して返す。
     * @return Todo のリスト(空の可能性あり)
     */
    fun findAll(): List<Todo> = repository.findAll()

    /**
     * 指定 ID の Todo を取得
     * 存在しない場合は null を返す(呼び出し元で 404 処理などを行う)
     * @param id 検索対象の ID
     * @return Todo または null
     */
    fun findById(id: Long): Todo? = repository.findById(id)

    /**
     * 新しい Todo を作成する。
     * - タイトルが空白の場合は IllegalArgumentException をスローする(引数チェック)
     * - リポジトリに保存する際に ID はリポジトリ側で付与される想定(新規は 0L を指定)
     * @param title Todo のタイトル(非空)
     * @return 保存済みの Todo(ID が付与されたエンティティ)
     */
    fun create(title: String): Todo {
        // タイトルは空文字や空白のみを許容しない
        require(title.isNotBlank()) { "title must not be blank" }
        // 新規作成のため id は 0L を指定し、保存処理でリポジトリが ID を振る
        val todo = Todo(0L, title)
        return repository.save(todo)
    }

    /**
     * 指定 ID の Todo を更新する(PATCH 用)。
     * - title が空白の場合は IllegalArgumentException をスローする
     * - 指定した ID が存在する場合は更新して更新済みエンティティを返す
     * - 指定した ID が存在しない場合は null を返す(コントローラ側で 404 を返すため)
     *
     * @param id 更新対象の ID
     * @param title 更新後のタイトル
     * @return 更新済み Todo または null
     */
    fun update(id: Long, title: String, completed:Boolean): Todo? {
        require(title.isNotBlank()) { "title must not be blank" }
        val existing = repository.findById(id) ?: return null
        val updated = existing.copy(title = title, completed = completed)
        return repository.save(updated)
    }

    /**
     * 指定 ID の Todo を削除する。
     * リポジトリの戻り値(削除の成否)をそのまま返す。
     * @param id 削除対象の ID
     * @return 削除に成功したら true、存在しなければ false
     */
    fun delete(id: Long): Boolean = repository.deleteById(id)
}

リポジトリの作成

最後に実際に処理を実行するクラス群を作成します

インターフェースの作成

こちらでは関数の宣言のみとなります

/**
 * Todo リポジトリの抽象インターフェース
 * このインターフェースはリポジトリが提供する基本的な CRUD 操作を定義
 */
interface TodoRepository {
    // すべての Todo を取得してリストで返す
    fun findAll(): List<Todo>

    /**
     * 指定した ID の Todo を返します。存在しない場合は null を返す
     * @param id 検索する Todo の識別子
     * @return Todo または null
     */
    fun findById(id: Long): Todo?

    /**
     * Todo を保存
     * - 新規作成の場合は保存後のエンティティ(ID が付与されたもの)を返す
     * - 更新の場合は更新後のエンティティを返す
     * @param todo 保存する Todo
     * @return 保存済みの Todo
     */
    fun save(todo: Todo): Todo

    /**
     * 指定した ID の Todo を削除します
     * 成功すれば true、対象が存在しなければ false を返す
     * @param id 削除対象の Todo の識別子
     * @return 削除できたかどうか
     */
    fun deleteById(id: Long): Boolean
}

In-memory 実装

こちらは実際に処理を記載します。今回はデータの保存はメモリ上としています。SQL等への保存は今回は実施しません

/**
 * In-memory 実装の TodoRepository。
 *
 * スレッドセーフな ConcurrentHashMap を内部ストレージとして使用し、
 * ID の発行には AtomicLong を使用
 */
@Repository
class InMemoryTodoRepository : TodoRepository {
    // 内部ストレージ: ID -> Todo
    private val storage = ConcurrentHashMap<Long, Todo>()
    // 新規 ID 発行用カウンタ。0 は未割り当てを表す。
    private val idCounter = AtomicLong(0)

    /**
     * すべての Todo を ID 昇順で返す
     * 空の場合は空のリストを返す
     */
    override fun findAll(): List<Todo> = storage.values.sortedBy { it.id }

    /**
     * 指定された ID の Todo を返す。存在しない場合は null を返す
     */
    override fun findById(id: Long): Todo? = storage[id]

    /**
     * Todo を保存する。
     * - todo.id == 0L の場合は新規作成を行い、新しい ID を割り当てて保存する
     * - todo.id != 0L の場合は更新処理を試みる
     *   - 指定された ID が既に存在する場合は上書き(更新)を行う
     *   - 指定された ID が存在しない場合は IllegalArgumentException を投げる
     *
     * この実装では「既存 ID の場合は更新のみ許可する」方針
     * (存在しない ID での保存を自動的に新規作成に切り替えない)
     *
     * @throws IllegalArgumentException 存在しない ID を指定して更新しようとした場合
     */
    override fun save(todo: Todo): Todo {
        return if (todo.id == 0L) {
            // 新規作成: 新しい ID を発行して保存
            val id = idCounter.incrementAndGet()
            val toSave = todo.copy(id = id)
            storage[id] = toSave
            toSave
        } else {
            // 更新: 指定 ID が存在する場合のみ上書き
            val id = todo.id
            if (!storage.containsKey(id)) {
                // 存在しない ID での更新は許可しない
                throw IllegalArgumentException("Todo with id=$id does not exist, cannot update")
            }
            val toSave = todo.copy(id = id)
            storage[id] = toSave
            toSave
        }
    }

    /**
     * 指定した ID の Todo を削除する。削除に成功すれば true、存在しなければ false を返す
     */
    override fun deleteById(id: Long): Boolean = storage.remove(id) != null
}

これで実際に実行すると以下のようになり、簡単にAPIサーバーの作成ができました
APIサーバーへのリクエストはPostmanを使用しています

Todoアイテム作成が問題なく実行され

アイテムの取得もでき

更新も問題なくでき

削除もできました。これで簡単ではありますがAPIサーバーをKotlinで立てることができました

会社紹介

私が所属しているアドバンスド・ソリューション株式会社(以下、ADS)は一緒に働く仲間を募集しています

会社概要
「技術」×「知恵」=顧客課題の解決・新しい価値の創造

この方程式の実現はADSが大切にしている考えで、技術を磨き続けるgeekさと、顧客を思うloveがあってこそ実現できる世界観だと思っています
この『love & geek』の精神さえあれば、得意不得意はno problem!
技術はピカイチだけど顧客折衝はちょっと苦手。OKです。技術はまだ未熟だけど顧客と知恵を出し合って要件定義するのは大好き。OKです
凸凹な社員の集まり、色んなカラーや柄の個性が集まっているからこそ、常に新しいソリューションが生まれています

ミッション
私たちは、テクノロジーを活用し、業務や事業の生産性向上と企業進化を支援します

ホームページ
アドバンスド・ソリューション株式会社|ADS Co., Ltd.
アドバンスド・ソリューション株式会社|ADS Co., Ltd.

Microsoft 365/SharePoint/Power Platform/Azure による DX コンサル・シス ...

サイトへ移動

PR

-プログラム, 技術
-,