M
Mr Sugiarto
Developer
02 Oct 2026 7 min read

Bab lalu kita menulis kontrak API Inventory Service sebelum implementasinya matang. Sekarang bayangkan kontrak itu sudah dipakai, Inventory Service sudah hidup di port 8081, dan Order Service sudah hidup di port 8082. Pertanyaannya: apakah client (browser, mobile app, atau service lain) harus tahu dan mengingat dua port berbeda itu? Bab ini menjawabnya dengan membangun satu pintu masuk tunggal — Gateway Service — dan yang lebih penting, membuktikan dengan curl sungguhan bahwa itu benar-benar bekerja.

Kenapa Butuh API Gateway?

Tanpa gateway, client harus tahu persis: "cek stok? panggil port 8081. Urus order? panggil port 8082." Itu menyebalkan sekaligus rapuh — begitu ada service baru, atau sebuah service pindah port, semua client harus di-update. Belum lagi kalau nanti ada kebutuhan lintas-service seperti autentikasi terpusat atau rate limiting — tanpa satu pintu masuk, hal-hal itu harus diduplikasi ke setiap service.

API Gateway menyelesaikan ini dengan jadi satu-satunya titik yang diketahui client: satu host, satu port (di project kita, port 8080), dan gateway itulah yang tahu ke mana setiap request harus diteruskan berdasarkan path-nya. Client tidak pernah perlu tahu Order Service ada di 8082 atau Inventory Service ada di 8081 — yang dia tahu cuma "kirim ke port 8080, sisanya urusan gateway."

Dependency: Spring Cloud Gateway Server MVC

Mari lihat gateway-service/build.gradle.kts:

plugins {
	kotlin("jvm") version "2.3.21"
	kotlin("plugin.spring") version "2.3.21"
	id("org.springframework.boot") version "4.1.1"
	id("io.spring.dependency-management") version "1.1.7"
}

group = "com.indokoding"
version = "0.0.1-SNAPSHOT"
description = "API Gateway - studi kasus routing terpusat, seri Belajar Kotlin Backend untuk Microservice"

java {
	toolchain {
		languageVersion = JavaLanguageVersion.of(21)
	}
}

repositories {
	mavenCentral()
}

extra["springCloudVersion"] = "2025.1.3"

dependencies {
	implementation("org.jetbrains.kotlin:kotlin-reflect")
	implementation("org.springframework.cloud:spring-cloud-starter-gateway-server-webmvc")
	testImplementation("org.springframework.boot:spring-boot-starter-test")
	testImplementation("org.jetbrains.kotlin:kotlin-test-junit5")
	testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}

dependencyManagement {
	imports {
		mavenBom("org.springframework.cloud:spring-cloud-dependencies:${property("springCloudVersion")}")
	}
}

Dua hal yang perlu diperhatikan. Pertama, dependency-nya adalah spring-cloud-starter-gateway-server-webmvc — perhatikan akhiran -webmvc. Ini bukan basa-basi penamaan: ini adalah Spring Cloud Gateway Server MVC, implementasi gateway yang lebih baru dan berjalan di atas Servlet/blocking I/O (Spring MVC biasa) — berbeda dari Spring Cloud Gateway "klasik" yang berbasis WebFlux/reactive. Kita pilih varian MVC ini karena cocok dengan gaya sinkron/blocking yang dipakai konsisten di seluruh seri ini — Order Service dan Inventory Service juga sama-sama pakai Spring MVC biasa, bukan WebFlux. Tidak ada reactive programming yang perlu dipelajari tambahan hanya untuk gateway ini.

Kedua, perhatikan dependencyManagement { imports { mavenBom(...) } } yang mengimpor spring-cloud-dependencies versi 2025.1.3 (didefinisikan lewat extra["springCloudVersion"]). Spring Cloud dirilis sebagai kumpulan modul yang versinya dikelola bersama lewat satu BOM (Bill of Materials) — dengan mengimpor BOM ini, kita tidak perlu menuliskan nomor versi manual untuk setiap dependency Spring Cloud yang dipakai; versinya otomatis selaras satu sama lain sesuai release train 2025.1.3.

Konfigurasi Port dan Target Service

gateway-service/src/main/resources/application.yml:

server:
  port: 8080

spring:
  application:
    name: gateway-service

services:
  order-service:
    uri: http://localhost:8082
  inventory-service:
    uri: http://localhost:8081

Gateway sendiri berjalan di port 8080 — port publik yang akan diketahui client. Dua baris terakhir, services.order-service.uri dan services.inventory-service.uri, adalah properti custom yang kita definisikan sendiri (bukan bawaan Spring Cloud Gateway) untuk menyimpan alamat kedua service backend. Properti custom ini yang akan dibaca oleh GatewayRouteConfig lewat @Value.

GatewayRouteConfig.kt — Routing Functional DSL

Ini bagian intinya. File gateway-service/src/main/kotlin/com/indokoding/gatewayservice/GatewayRouteConfig.kt secara lengkap:

package com.indokoding.gatewayservice

import org.springframework.beans.factory.annotation.Value
import org.springframework.cloud.gateway.server.mvc.filter.BeforeFilterFunctions.uri
import org.springframework.cloud.gateway.server.mvc.handler.GatewayRouterFunctions.route
import org.springframework.cloud.gateway.server.mvc.handler.HandlerFunctions.http
import org.springframework.cloud.gateway.server.mvc.predicate.GatewayRequestPredicates.path
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.web.servlet.function.RouterFunction
import org.springframework.web.servlet.function.ServerResponse

// Fase 4.4 - API Gateway: satu pintu masuk (port 8080, lihat application.yml) yang
// meneruskan request ke service yang tepat berdasarkan path - client tidak perlu tahu
// Order Service ada di port 8082 dan Inventory Service di port 8081.
@Configuration
class GatewayRouteConfig(
    @Value("\${services.order-service.uri}") private val orderServiceUri: String,
    @Value("\${services.inventory-service.uri}") private val inventoryServiceUri: String
) {

    @Bean
    fun orderServiceRoute(): RouterFunction<ServerResponse> =
        route("order-service")
            .route(path("/api/orders/**"), http())
            .before(uri(orderServiceUri))
            .build()

    @Bean
    fun inventoryServiceRoute(): RouterFunction<ServerResponse> =
        route("inventory-service")
            .route(path("/api/inventory/**"), http())
            .before(uri(inventoryServiceUri))
            .build()
}

Constructor-nya membaca dua properti dari application.yml lewat @Value("\${services.order-service.uri}") dan @Value("\${services.inventory-service.uri}") — ini cara paling sederhana untuk menyuntikkan satu nilai konfigurasi (dibanding @ConfigurationProperties yang lebih cocok untuk grup properti yang lebih besar, seperti yang dipakai InventoryServiceProperties di Order Service — akan kita lihat lagi di bab 29).

Bagian paling penting untuk dipahami betul adalah rantai builder ini, baris demi baris:

  • GatewayRouterFunctions.route("order-service") — memulai sebuah route builder, diberi nama "order-service" (nama ini cuma untuk identifikasi/logging, tidak memengaruhi routing itu sendiri).
  • .route(path("/api/orders/**"), http()) — mendaftarkan satu route: kalau path request cocok dengan predicate path("/api/orders/**") (artinya semua path yang diawali /api/orders/), maka route ini ditangani oleh http(). Yang penting dipahami: http() di sini adalah handler generik — ia tahu cara meneruskan (proxy) request HTTP ke suatu tujuan, tapi belum tahu tujuannya ke mana. http() yang sama persis dipakai lagi di route kedua untuk Inventory Service — ia reusable, tidak spesifik ke satu service.
  • .before(uri(orderServiceUri)) — inilah yang membuat route ini spesifik menunjuk ke Order Service. .before(...) mendaftarkan sebuah filter yang dijalankan sebelum handler (http()) dieksekusi, dan uri(orderServiceUri) mengisi target URI aktual (http://localhost:8082, dari application.yml) ke dalam request yang sedang diproses. Jadi urutannya: request masuk → filter uri(...) menetapkan ke mana ia harus diteruskan → handler generik http() benar-benar melakukan proxy-nya ke sana.
  • .build() — merangkai semuanya jadi satu RouterFunction<ServerResponse>, dideklarasikan sebagai @Bean agar dikenali Spring.

Kenapa dipisah jadi dua tahap seperti ini (predicate+handler generik, lalu filter yang mengisi tujuan), bukan digabung jadi satu langkah "arahkan path ini ke URL itu"? Karena desain ini reusable dan komposabel — http() sebagai handler proxy generik bisa dipasangkan dengan filter tujuan yang berbeda-beda tanpa perlu implementasi proxy yang berulang, dan kamu bisa menambahkan filter .before(...) atau .after(...) lain di antaranya (misalnya untuk menambah header, logging, atau — kalau nanti masuk ke Fase 6/7 — autentikasi) tanpa mengubah bagian proxy-nya sama sekali.

Route kedua, inventoryServiceRoute(), persis pola yang sama: path predicate-nya /api/inventory/** (bukan /api/orders/**), dan tujuannya inventoryServiceUri (http://localhost:8081).

Bukti Nyata: Jalankan Sendiri dan Lihat

Bagian ini yang paling penting di bab ini — bukan sekadar percaya bahwa routing-nya "seharusnya jalan", tapi benar-benar membuktikannya. Buka dua terminal.

Terminal pertama, jalankan Inventory Service:

cd inventory-service
./gradlew bootRun

Terminal kedua, jalankan Gateway Service:

cd gateway-service
./gradlew bootRun

Begitu keduanya hidup — Inventory Service di port 8081, Gateway Service di port 8080 — coba tiga curl berikut, persis seperti yang dijalankan langsung untuk memverifikasi project ini:

curl http://localhost:8081/api/inventory/Keyboard%20Mechanical
→ {"itemName":"Keyboard Mechanical","stock":12}  HTTP 200   (langsung ke Inventory Service)

curl http://localhost:8080/api/inventory/Keyboard%20Mechanical
→ {"itemName":"Keyboard Mechanical","stock":12}  HTTP 200   (hasil sama, tapi lewat Gateway di port 8080)

curl http://localhost:8080/api/inventory/Unknown%20Item
→ {"error":"Item 'Unknown Item' tidak terdaftar di Inventory Service"}  HTTP 404   (response error pun ikut diteruskan dengan benar)

Perhatikan curl kedua dan ketiga — keduanya menembak port 8080 (Gateway), bukan 8081 (Inventory Service langsung), tapi hasilnya identik dengan memanggil Inventory Service secara langsung. Itu bukti konkret bahwa GatewayRouteConfig bekerja: request yang masuk lewat pintu tunggal (8080) benar-benar diteruskan ke backend yang tepat (8081) berdasarkan path /api/inventory/**, lengkap dengan status code dan body error yang ikut terbawa apa adanya — bukan cuma diteruskan saat sukses. Test suite gateway-service sendiri (./gradlew test) juga hijau untuk memverifikasi context Spring-nya bisa dimuat dengan benar.

Dengan Gateway Service ini berdiri sebagai pintu masuk tunggal, satu potongan terakhir yang belum kita bahas adalah kode di sisi Order Service yang benar-benar memanggil Inventory Service — lengkap dengan timeout dan penanganan kegagalan. Itu topik bab terakhir seri ini.

M
Mr Sugiarto

Developer

Bagian dari Series: Kotlin Backend Microservice - Belajar Fundamental sampai Microservice Nyata

Belajar Kotlin + Spring Boot dari fundamental bahasa (null safety, OOP, coroutines) sampai membangun sistem microservice sungguhan - REST API, Postgre...

Lihat Series Lengkap
Artikel Terkait
Artikel Sebelumnya
API Contract-First Design