Introduction: Is REST Dead? Long Live GraphQL? (Spoiler: No)
\nIf you write in Kotlin and develop a backend, you've certainly heard the holy war: GraphQL vs REST. In one corner of the ring is good old REST, which every junior knows. In the other is trendy GraphQL, which promises "give me what I want, but I don't know what, just in one query."
\nI am a backend developer with experience in Kotlin, and over the past couple of years I've rewritten three projects from REST to GraphQL and back. Spoiler: each tool is good for its own task. In this article, we won't be fanboys. Instead, we'll analyze real code examples of when REST on Kotlin wins, and when GraphQL is your savior.
\nLet's dive into the comparison of GraphQL and REST in Kotlin without fluff and marketing.
\n\n1. Briefly About the Main Thing: What is What
\nBefore jumping into code, let's agree on terms. REST API is an architectural style where each resource (user, order, product) has its own URL and HTTP methods (GET, POST, PUT, DELETE). Kotlin developers most often use Spring Boot or Ktor to create REST services.
\nGraphQL is a query language where you have one endpoint (usually /graphql), and the client itself decides which fields it needs. In Kotlin, popular libraries are KGraphQL (lightweight) and Spring for GraphQL (for Spring fans).
The key difference: REST is "the server dictates what to return." GraphQL is "the client asks only for what it needs."
\n\n2. Example of a REST API in Kotlin (Ktor)
\nLet's start with the classics. Suppose we have an online store. We need to get data about a user and their recent orders.
\nHere's what a typical REST controller looks like on Ktor (a lightweight framework for Kotlin):
\n\n// REST endpoint: GET /api/users/{id}\nfun Application.userModule() {\n routing {\n get("/api/users/{id}") {\n val userId = call.parameters["id"]?.toIntOrNull() ?: return@get call.respondText("Bad Request", status = HttpStatusCode.BadRequest)\n val user = userService.getUserById(userId) ?: return@get call.respondText("Not Found", status = HttpStatusCode.NotFound)\n call.respond(user)\n }\n }\n}\n\n// Data class for the response\n@Serializable\ndata class UserResponse(\n val id: Int,\n val name: String,\n val email: String,\n val lastOrders: List<Order>\n)\n\nThe problem: if the client (mobile app) only needs the user's email, the server still returns the full UserResponse object with orders. This is over-fetching — redundant data transfer. On weak mobile networks, this hurts.
Solution number one is to make several small endpoints: GET /api/users/{id}/email. But then the number of requests grows (under-fetching).
3. Example of GraphQL in Kotlin (Spring for GraphQL)
\nNow the same scenario, but with GraphQL. Here is the schema (schema.graphqls):
\n\ntype Query {\n user(id: ID!): User\n}\n\ntype User {\n id: ID!\n name: String\n email: String\n lastOrders: [Order]\n}\n\ntype Order {\n id: ID!\n total: Float\n date: String\n}\n\nAnd this is the Kotlin code using Spring for GraphQL (annotation approach):
\n\n@Controller\nclass UserController(private val userService: UserService) {\n\n @QueryMapping\n fun user(@Argument id: Int): User {\n return userService.getUserById(id) ?: throw UserNotFoundException("User not found")\n }\n}\n\n// Data class without extra fields\ndata class User(val id: Int, val name: String, val email: String, val lastOrders: List<Order>)\n\nNow the client can request only what it needs:
\n\nquery {\n user(id: 42) {\n email\n }\n}