In modern microservices, efficient and reliable inter-service communication is paramount. gRPC, an open-source Remote Procedure Call (RPC) framework developed by Google, leverages HTTP/2 and Protocol Buffers (protobuf) to provide high-performance, strongly-typed service interactions. This guide offers a detailed, step-by-step walkthrough of building a Todo application using Spring Boot 3, Spring gRPC (currently experimental), Kotlin, and MongoDB. We’ll implement the gRPC server first, followed by the client, ensuring a clear, sequential development process.
What is gRPC?
gRPC is a high-performance RPC framework that uses HTTP/2 for transport and Protocol Buffers for defining services and messages. It supports unary calls (single request-response) and streaming (client, server, or bidirectional), making it ideal for microservices requiring low latency and high throughput. Unlike REST, which typically uses JSON over HTTP/1.1, gRPC’s binary serialization and contract-first design enhance efficiency and reliability.
Benefits of gRPC Over REST
- Performance: HTTP/2’s multiplexing and header compression, paired with Protobuf’s compact binary format, outperform JSON-based REST APIs.
- Strong Typing: Protobuf enforces a schema, reducing runtime errors compared to REST’s loosely typed payloads.
- Streaming: gRPC supports real-time communication via streaming, unlike REST’s static request-response model.
- Code Generation: Protobuf generates client and server code across multiple languages, streamlining development.
- Interoperability: Its language-agnostic nature suits polyglot environments.
While REST is simpler and browser-friendly, gRPC excels in internal service communication for performance-critical systems.
Key Terms in gRPC
- Protocol Buffers (protobuf): A serialization format and Interface Definition Language (IDL) for defining services and messages.
- Service: A set of RPC methods (e.g.,
CreateTodo
) defined in a.proto
file. - Stub: Auto-generated code for clients to call server methods (e.g.,
TodoServiceGrpc.TodoServiceBlockingStub
). - Channel: An abstraction over the HTTP/2 connection between client and server.
- Unary RPC: A single request-response interaction, used in this guide.
- Streaming RPC: Continuous data exchange, not covered here but supported by gRPC.
gRPC Server Implementation
Let’s build the grpc-server
to manage Todo operations and persist data in MongoDB.
Step 1: Project Setup
-
Create the Project:
- Use Spring Initializr (start.spring.io) to generate a Spring Boot 3 project with Kotlin.
- I have set the group to
com.mahmud
, artifact togrpc-server
, and selected Kotlin as language, project type Gradle (Kotlin).
-
Add Dependencies:
- include:
spring-boot-starter-data-mongodb-reactive
spring-grpc-spring-boot-starter
- include:
-
Configure MongoDB:
- Ensure MongoDB is running locally
mongodb://localhost:27017
. - Create
application.yaml
:
<span>spring</span><span>:</span><span>application</span><span>:</span><span>name</span><span>:</span> <span>grpc-server</span><span>data</span><span>:</span><span>mongodb</span><span>:</span><span>uri</span><span>:</span> <span>mongodb://localhost:27017/todo_db</span><span>spring</span><span>:</span> <span>application</span><span>:</span> <span>name</span><span>:</span> <span>grpc-server</span> <span>data</span><span>:</span> <span>mongodb</span><span>:</span> <span>uri</span><span>:</span> <span>mongodb://localhost:27017/todo_db</span>
spring: application: name: grpc-server data: mongodb: uri: mongodb://localhost:27017/todo_db
- Ensure MongoDB is running locally
Step 2: Define the Protobuf File
Create todo.proto
to define the service and messages:
<span>syntax</span> <span>=</span> <span>"proto3"</span><span>;</span><span>package</span> <span>todo</span><span>;</span><span>option</span> <span>java_multiple_files</span> <span>=</span> <span>true</span><span>;</span><span>option</span> <span>java_package</span> <span>=</span> <span>"com.mahmud.grpcserver"</span><span>;</span><span>option</span> <span>java_outer_classname</span> <span>=</span> <span>"TodoProto"</span><span>;</span><span>// The Todo service definition</span><span>service</span> <span>TodoService</span> <span>{</span><span>// Create a new todo item</span><span>rpc</span> <span>CreateTodo</span> <span>(</span><span>TodoRequest</span><span>)</span> <span>returns</span> <span>(</span><span>TodoResponse</span><span>)</span> <span>{}</span><span>// Get a todo item by ID</span><span>rpc</span> <span>GetTodo</span> <span>(</span><span>TodoIdRequest</span><span>)</span> <span>returns</span> <span>(</span><span>TodoResponse</span><span>)</span> <span>{}</span><span>// List all todo items</span><span>rpc</span> <span>ListTodos</span> <span>(</span><span>ListTodosRequest</span><span>)</span> <span>returns</span> <span>(</span><span>ListTodosResponse</span><span>)</span> <span>{}</span><span>// Update a todo item</span><span>rpc</span> <span>UpdateTodo</span> <span>(</span><span>TodoRequest</span><span>)</span> <span>returns</span> <span>(</span><span>TodoResponse</span><span>)</span> <span>{}</span><span>// Delete a todo item</span><span>rpc</span> <span>DeleteTodo</span> <span>(</span><span>TodoIdRequest</span><span>)</span> <span>returns</span> <span>(</span><span>Empty</span><span>)</span> <span>{}</span><span>}</span><span>// Message definitions</span><span>message</span> <span>TodoItem</span> <span>{</span><span>string</span> <span>id</span> <span>=</span> <span>1</span><span>;</span><span>string</span> <span>title</span> <span>=</span> <span>2</span><span>;</span><span>string</span> <span>description</span> <span>=</span> <span>3</span><span>;</span><span>bool</span> <span>completed</span> <span>=</span> <span>4</span><span>;</span><span>google.protobuf.Timestamp</span> <span>created_at</span> <span>=</span> <span>5</span><span>;</span><span>google.protobuf.Timestamp</span> <span>updated_at</span> <span>=</span> <span>6</span><span>;</span><span>optional</span> <span>google.protobuf.Timestamp</span> <span>due_date</span> <span>=</span> <span>7</span><span>;</span><span>}</span><span>message</span> <span>TodoRequest</span> <span>{</span><span>string</span> <span>id</span> <span>=</span> <span>1</span><span>;</span> <span>// Empty for create, required for update</span><span>string</span> <span>title</span> <span>=</span> <span>2</span><span>;</span><span>string</span> <span>description</span> <span>=</span> <span>3</span><span>;</span><span>bool</span> <span>completed</span> <span>=</span> <span>4</span><span>;</span><span>optional</span> <span>google.protobuf.Timestamp</span> <span>due_date</span> <span>=</span> <span>5</span><span>;</span><span>}</span><span>message</span> <span>TodoResponse</span> <span>{</span><span>TodoItem</span> <span>todo</span> <span>=</span> <span>1</span><span>;</span><span>}</span><span>message</span> <span>TodoIdRequest</span> <span>{</span><span>string</span> <span>id</span> <span>=</span> <span>1</span><span>;</span><span>}</span><span>message</span> <span>ListTodosRequest</span> <span>{</span><span>optional</span> <span>bool</span> <span>show_completed</span> <span>=</span> <span>1</span><span>;</span> <span>// Filter for completed todos</span><span>int32</span> <span>page_size</span> <span>=</span> <span>2</span><span>;</span> <span>// Number of items per page</span><span>string</span> <span>page_token</span> <span>=</span> <span>3</span><span>;</span> <span>// Pagination token</span><span>}</span><span>message</span> <span>ListTodosResponse</span> <span>{</span><span>repeated</span> <span>TodoItem</span> <span>todos</span> <span>=</span> <span>1</span><span>;</span><span>string</span> <span>next_page_token</span> <span>=</span> <span>2</span><span>;</span><span>}</span><span>message</span> <span>Empty</span> <span>{}</span><span>// Import Google timestamp for timestamp fields</span><span>import</span> <span>"google/protobuf/timestamp.proto"</span><span>;</span><span>syntax</span> <span>=</span> <span>"proto3"</span><span>;</span> <span>package</span> <span>todo</span><span>;</span> <span>option</span> <span>java_multiple_files</span> <span>=</span> <span>true</span><span>;</span> <span>option</span> <span>java_package</span> <span>=</span> <span>"com.mahmud.grpcserver"</span><span>;</span> <span>option</span> <span>java_outer_classname</span> <span>=</span> <span>"TodoProto"</span><span>;</span> <span>// The Todo service definition</span> <span>service</span> <span>TodoService</span> <span>{</span> <span>// Create a new todo item</span> <span>rpc</span> <span>CreateTodo</span> <span>(</span><span>TodoRequest</span><span>)</span> <span>returns</span> <span>(</span><span>TodoResponse</span><span>)</span> <span>{}</span> <span>// Get a todo item by ID</span> <span>rpc</span> <span>GetTodo</span> <span>(</span><span>TodoIdRequest</span><span>)</span> <span>returns</span> <span>(</span><span>TodoResponse</span><span>)</span> <span>{}</span> <span>// List all todo items</span> <span>rpc</span> <span>ListTodos</span> <span>(</span><span>ListTodosRequest</span><span>)</span> <span>returns</span> <span>(</span><span>ListTodosResponse</span><span>)</span> <span>{}</span> <span>// Update a todo item</span> <span>rpc</span> <span>UpdateTodo</span> <span>(</span><span>TodoRequest</span><span>)</span> <span>returns</span> <span>(</span><span>TodoResponse</span><span>)</span> <span>{}</span> <span>// Delete a todo item</span> <span>rpc</span> <span>DeleteTodo</span> <span>(</span><span>TodoIdRequest</span><span>)</span> <span>returns</span> <span>(</span><span>Empty</span><span>)</span> <span>{}</span> <span>}</span> <span>// Message definitions</span> <span>message</span> <span>TodoItem</span> <span>{</span> <span>string</span> <span>id</span> <span>=</span> <span>1</span><span>;</span> <span>string</span> <span>title</span> <span>=</span> <span>2</span><span>;</span> <span>string</span> <span>description</span> <span>=</span> <span>3</span><span>;</span> <span>bool</span> <span>completed</span> <span>=</span> <span>4</span><span>;</span> <span>google.protobuf.Timestamp</span> <span>created_at</span> <span>=</span> <span>5</span><span>;</span> <span>google.protobuf.Timestamp</span> <span>updated_at</span> <span>=</span> <span>6</span><span>;</span> <span>optional</span> <span>google.protobuf.Timestamp</span> <span>due_date</span> <span>=</span> <span>7</span><span>;</span> <span>}</span> <span>message</span> <span>TodoRequest</span> <span>{</span> <span>string</span> <span>id</span> <span>=</span> <span>1</span><span>;</span> <span>// Empty for create, required for update</span> <span>string</span> <span>title</span> <span>=</span> <span>2</span><span>;</span> <span>string</span> <span>description</span> <span>=</span> <span>3</span><span>;</span> <span>bool</span> <span>completed</span> <span>=</span> <span>4</span><span>;</span> <span>optional</span> <span>google.protobuf.Timestamp</span> <span>due_date</span> <span>=</span> <span>5</span><span>;</span> <span>}</span> <span>message</span> <span>TodoResponse</span> <span>{</span> <span>TodoItem</span> <span>todo</span> <span>=</span> <span>1</span><span>;</span> <span>}</span> <span>message</span> <span>TodoIdRequest</span> <span>{</span> <span>string</span> <span>id</span> <span>=</span> <span>1</span><span>;</span> <span>}</span> <span>message</span> <span>ListTodosRequest</span> <span>{</span> <span>optional</span> <span>bool</span> <span>show_completed</span> <span>=</span> <span>1</span><span>;</span> <span>// Filter for completed todos</span> <span>int32</span> <span>page_size</span> <span>=</span> <span>2</span><span>;</span> <span>// Number of items per page</span> <span>string</span> <span>page_token</span> <span>=</span> <span>3</span><span>;</span> <span>// Pagination token</span> <span>}</span> <span>message</span> <span>ListTodosResponse</span> <span>{</span> <span>repeated</span> <span>TodoItem</span> <span>todos</span> <span>=</span> <span>1</span><span>;</span> <span>string</span> <span>next_page_token</span> <span>=</span> <span>2</span><span>;</span> <span>}</span> <span>message</span> <span>Empty</span> <span>{}</span> <span>// Import Google timestamp for timestamp fields</span> <span>import</span> <span>"google/protobuf/timestamp.proto"</span><span>;</span>syntax = "proto3"; package todo; option java_multiple_files = true; option java_package = "com.mahmud.grpcserver"; option java_outer_classname = "TodoProto"; // The Todo service definition service TodoService { // Create a new todo item rpc CreateTodo (TodoRequest) returns (TodoResponse) {} // Get a todo item by ID rpc GetTodo (TodoIdRequest) returns (TodoResponse) {} // List all todo items rpc ListTodos (ListTodosRequest) returns (ListTodosResponse) {} // Update a todo item rpc UpdateTodo (TodoRequest) returns (TodoResponse) {} // Delete a todo item rpc DeleteTodo (TodoIdRequest) returns (Empty) {} } // Message definitions message TodoItem { string id = 1; string title = 2; string description = 3; bool completed = 4; google.protobuf.Timestamp created_at = 5; google.protobuf.Timestamp updated_at = 6; optional google.protobuf.Timestamp due_date = 7; } message TodoRequest { string id = 1; // Empty for create, required for update string title = 2; string description = 3; bool completed = 4; optional google.protobuf.Timestamp due_date = 5; } message TodoResponse { TodoItem todo = 1; } message TodoIdRequest { string id = 1; } message ListTodosRequest { optional bool show_completed = 1; // Filter for completed todos int32 page_size = 2; // Number of items per page string page_token = 3; // Pagination token } message ListTodosResponse { repeated TodoItem todos = 1; string next_page_token = 2; } message Empty {} // Import Google timestamp for timestamp fields import "google/protobuf/timestamp.proto";
Enter fullscreen mode Exit fullscreen mode
The proto defines the TodoService
with five unary RPC methods for CRUD operations. Messages like TodoItem
and TodoRequest
use Protobuf scalars and Timestamp
for date-time fields.
Breakdown:
-
service TodoService
: Specifies CRUD methods. -
TodoItem
: Represents a Todo with fields likeid
and optionaldue_date
. -
repeated
: Indicates a list (e.g.,todos
inListTodosResponse
). -
optional
: Marks nullable fields.
Step 3: Generate gRPC Stubs
Run:
./gradlew build./gradlew build./gradlew build
Enter fullscreen mode Exit fullscreen mode
This compiles the Protobuf file and generates stubs in build/generated/source/proto/main
, including TodoServiceGrpc.java
. In IntelliJ IDEA, right-click the generated folder and select “Mark Directory As → Generated Source Root”.
Step 4: Implement Server Logic
Create the domain model. It defines the Todo
entity for MongoDB persistence.
Todo.kt
<span>package</span> <span>com.mahmud.grpcserver.model</span><span>import</span> <span>org.springframework.data.annotation.Id</span><span>import</span> <span>org.springframework.data.mongodb.core.mapping.Document</span><span>import</span> <span>java.time.Instant</span><span>@Document</span><span>(</span><span>collection</span> <span>=</span> <span>"todos"</span><span>)</span><span>data class</span> <span>Todo</span><span>(</span><span>@Id</span><span>val</span> <span>id</span><span>:</span> <span>String</span><span>?</span> <span>=</span> <span>null</span><span>,</span><span>val</span> <span>title</span><span>:</span> <span>String</span><span>,</span><span>val</span> <span>description</span><span>:</span> <span>String</span><span>,</span><span>val</span> <span>completed</span><span>:</span> <span>Boolean</span> <span>=</span> <span>false</span><span>,</span><span>val</span> <span>createdAt</span><span>:</span> <span>Instant</span> <span>=</span> <span>Instant</span><span>.</span><span>now</span><span>(),</span><span>val</span> <span>updatedAt</span><span>:</span> <span>Instant</span> <span>=</span> <span>Instant</span><span>.</span><span>now</span><span>(),</span><span>val</span> <span>dueDate</span><span>:</span> <span>Instant</span><span>?</span> <span>=</span> <span>null</span><span>)</span><span>package</span> <span>com.mahmud.grpcserver.model</span> <span>import</span> <span>org.springframework.data.annotation.Id</span> <span>import</span> <span>org.springframework.data.mongodb.core.mapping.Document</span> <span>import</span> <span>java.time.Instant</span> <span>@Document</span><span>(</span><span>collection</span> <span>=</span> <span>"todos"</span><span>)</span> <span>data class</span> <span>Todo</span><span>(</span> <span>@Id</span> <span>val</span> <span>id</span><span>:</span> <span>String</span><span>?</span> <span>=</span> <span>null</span><span>,</span> <span>val</span> <span>title</span><span>:</span> <span>String</span><span>,</span> <span>val</span> <span>description</span><span>:</span> <span>String</span><span>,</span> <span>val</span> <span>completed</span><span>:</span> <span>Boolean</span> <span>=</span> <span>false</span><span>,</span> <span>val</span> <span>createdAt</span><span>:</span> <span>Instant</span> <span>=</span> <span>Instant</span><span>.</span><span>now</span><span>(),</span> <span>val</span> <span>updatedAt</span><span>:</span> <span>Instant</span> <span>=</span> <span>Instant</span><span>.</span><span>now</span><span>(),</span> <span>val</span> <span>dueDate</span><span>:</span> <span>Instant</span><span>?</span> <span>=</span> <span>null</span> <span>)</span>package com.mahmud.grpcserver.model import org.springframework.data.annotation.Id import org.springframework.data.mongodb.core.mapping.Document import java.time.Instant @Document(collection = "todos") data class Todo( @Id val id: String? = null, val title: String, val description: String, val completed: Boolean = false, val createdAt: Instant = Instant.now(), val updatedAt: Instant = Instant.now(), val dueDate: Instant? = null )
Enter fullscreen mode Exit fullscreen mode
Breakdown:
-
@Document(collection = "todos")
: Maps to thetodos
collection. -
@Id
: Unique identifier, nullable for new entries. -
Instant
: Matches Protobuf’sTimestamp
.
Create the repository interface. It provides reactive CRUD operations with a custom filter method.
TodoRepository.kt
<span>package</span> <span>com.mahmud.grpcserver.repository</span><span>import</span> <span>com.mahmud.grpcserver.model.Todo</span><span>import</span> <span>org.springframework.data.mongodb.repository.ReactiveMongoRepository</span><span>import</span> <span>reactor.core.publisher.Flux</span><span>interface</span> <span>TodoRepository</span> <span>:</span> <span>ReactiveMongoRepository</span><span><</span><span>Todo</span><span>,</span> <span>String</span><span>></span> <span>{</span><span>fun</span> <span>findByCompleted</span><span>(</span><span>completed</span><span>:</span> <span>Boolean</span><span>):</span> <span>Flux</span><span><</span><span>Todo</span><span>></span><span>}</span><span>package</span> <span>com.mahmud.grpcserver.repository</span> <span>import</span> <span>com.mahmud.grpcserver.model.Todo</span> <span>import</span> <span>org.springframework.data.mongodb.repository.ReactiveMongoRepository</span> <span>import</span> <span>reactor.core.publisher.Flux</span> <span>interface</span> <span>TodoRepository</span> <span>:</span> <span>ReactiveMongoRepository</span><span><</span><span>Todo</span><span>,</span> <span>String</span><span>></span> <span>{</span> <span>fun</span> <span>findByCompleted</span><span>(</span><span>completed</span><span>:</span> <span>Boolean</span><span>):</span> <span>Flux</span><span><</span><span>Todo</span><span>></span> <span>}</span>package com.mahmud.grpcserver.repository import com.mahmud.grpcserver.model.Todo import org.springframework.data.mongodb.repository.ReactiveMongoRepository import reactor.core.publisher.Flux interface TodoRepository : ReactiveMongoRepository<Todo, String> { fun findByCompleted(completed: Boolean): Flux<Todo> }
Enter fullscreen mode Exit fullscreen mode
Breakdown:
-
ReactiveMongoRepository<Todo, String>
: Manages Todo entities. -
Flux<Todo>
: Returns a reactive stream.
Implement the gRPC service. It will handle our main logics.
TodoService.kt
<span>package</span> <span>com.mahmud.grpcserver.service</span><span>import</span> <span>com.mahmud.grpcserver.model.Todo</span><span>import</span> <span>com.google.protobuf.Timestamp</span><span>import</span> <span>com.mahmud.grpcserver.*</span><span>import</span> <span>com.mahmud.grpcserver.repository.TodoRepository</span><span>import</span> <span>io.grpc.stub.StreamObserver</span><span>import</span> <span>org.springframework.beans.factory.annotation.Autowired</span><span>import</span> <span>org.springframework.grpc.server.service.GrpcService</span><span>import</span> <span>reactor.core.publisher.Mono</span><span>import</span> <span>java.time.Instant</span><span>@GrpcService</span><span>class</span> <span>TodoService</span> <span>@Autowired</span> <span>constructor</span><span>(</span><span>private</span> <span>val</span> <span>todoRepository</span><span>:</span> <span>TodoRepository</span><span>)</span> <span>:</span> <span>TodoServiceGrpc</span><span>.</span><span>TodoServiceImplBase</span><span>()</span> <span>{</span><span>// gRPC method to create a new Todo item</span><span>override</span> <span>fun</span> <span>createTodo</span><span>(</span><span>request</span><span>:</span> <span>TodoRequest</span><span>,</span> <span>responseObserver</span><span>:</span> <span>StreamObserver</span><span><</span><span>TodoResponse</span><span>>)</span> <span>{</span><span>val</span> <span>todo</span> <span>=</span> <span>Todo</span><span>(</span><span>title</span> <span>=</span> <span>request</span><span>.</span><span>title</span><span>,</span><span>description</span> <span>=</span> <span>request</span><span>.</span><span>description</span><span>,</span><span>completed</span> <span>=</span> <span>request</span><span>.</span><span>completed</span><span>,</span><span>dueDate</span> <span>=</span> <span>request</span><span>.</span><span>dueDate</span><span>?.</span><span>toInstant</span><span>()</span> <span>// Convert protobuf Timestamp to Java Instant</span><span>)</span><span>// Save Todo to database using reactive programming</span><span>todoRepository</span><span>.</span><span>save</span><span>(</span><span>todo</span><span>)</span><span>.</span><span>map</span> <span>{</span> <span>TodoResponse</span><span>.</span><span>newBuilder</span><span>().</span><span>setTodo</span><span>(</span><span>it</span><span>.</span><span>toProto</span><span>()).</span><span>build</span><span>()</span> <span>}</span><span>.</span><span>subscribe</span><span>(</span><span>{</span> <span>response</span> <span>-></span> <span>responseObserver</span><span>.</span><span>onNext</span><span>(</span><span>response</span><span>);</span> <span>responseObserver</span><span>.</span><span>onCompleted</span><span>()</span> <span>},</span><span>{</span> <span>error</span> <span>-></span> <span>responseObserver</span><span>.</span><span>onError</span><span>(</span><span>error</span><span>)</span> <span>}</span><span>)</span><span>}</span><span>// gRPC method to retrieve a Todo by its ID</span><span>override</span> <span>fun</span> <span>getTodo</span><span>(</span><span>request</span><span>:</span> <span>TodoIdRequest</span><span>,</span> <span>responseObserver</span><span>:</span> <span>StreamObserver</span><span><</span><span>TodoResponse</span><span>>)</span> <span>{</span><span>todoRepository</span><span>.</span><span>findById</span><span>(</span><span>request</span><span>.</span><span>id</span><span>)</span><span>.</span><span>switchIfEmpty</span><span>(</span><span>Mono</span><span>.</span><span>error</span><span>(</span><span>RuntimeException</span><span>(</span><span>"Todo not found"</span><span>)))</span> <span>// Handle missing Todo</span><span>.</span><span>map</span> <span>{</span> <span>TodoResponse</span><span>.</span><span>newBuilder</span><span>().</span><span>setTodo</span><span>(</span><span>it</span><span>.</span><span>toProto</span><span>()).</span><span>build</span><span>()</span> <span>}</span><span>.</span><span>subscribe</span><span>(</span><span>{</span> <span>response</span> <span>-></span> <span>responseObserver</span><span>.</span><span>onNext</span><span>(</span><span>response</span><span>);</span> <span>responseObserver</span><span>.</span><span>onCompleted</span><span>()</span> <span>},</span><span>{</span> <span>error</span> <span>-></span> <span>responseObserver</span><span>.</span><span>onError</span><span>(</span><span>error</span><span>)</span> <span>}</span><span>)</span><span>}</span><span>// gRPC method to list all Todos with an optional filter for completed ones</span><span>override</span> <span>fun</span> <span>listTodos</span><span>(</span><span>request</span><span>:</span> <span>ListTodosRequest</span><span>,</span> <span>responseObserver</span><span>:</span> <span>StreamObserver</span><span><</span><span>ListTodosResponse</span><span>>)</span> <span>{</span><span>val</span> <span>todos</span> <span>=</span> <span>if</span> <span>(</span><span>request</span><span>.</span><span>showCompleted</span><span>)</span> <span>{</span><span>todoRepository</span><span>.</span><span>findAll</span><span>()</span> <span>// Fetch all Todos</span><span>}</span> <span>else</span> <span>{</span><span>todoRepository</span><span>.</span><span>findByCompleted</span><span>(</span><span>false</span><span>)</span> <span>// Fetch only incomplete Todos</span><span>}</span><span>todos</span><span>.</span><span>collectList</span><span>()</span><span>.</span><span>map</span> <span>{</span> <span>items</span> <span>-></span><span>ListTodosResponse</span><span>.</span><span>newBuilder</span><span>()</span><span>.</span><span>addAllTodos</span><span>(</span><span>items</span><span>.</span><span>map</span> <span>{</span> <span>it</span><span>.</span><span>toProto</span><span>()</span> <span>})</span> <span>// Convert to protobuf format</span><span>.</span><span>build</span><span>()</span><span>}</span><span>.</span><span>subscribe</span><span>(</span><span>{</span> <span>response</span> <span>-></span> <span>responseObserver</span><span>.</span><span>onNext</span><span>(</span><span>response</span><span>);</span> <span>responseObserver</span><span>.</span><span>onCompleted</span><span>()</span> <span>},</span><span>{</span> <span>error</span> <span>-></span> <span>responseObserver</span><span>.</span><span>onError</span><span>(</span><span>error</span><span>)</span> <span>}</span><span>)</span><span>}</span><span>// gRPC method to update an existing Todo item</span><span>override</span> <span>fun</span> <span>updateTodo</span><span>(</span><span>request</span><span>:</span> <span>TodoRequest</span><span>,</span> <span>responseObserver</span><span>:</span> <span>StreamObserver</span><span><</span><span>TodoResponse</span><span>>)</span> <span>{</span><span>todoRepository</span><span>.</span><span>findById</span><span>(</span><span>request</span><span>.</span><span>id</span><span>)</span><span>.</span><span>switchIfEmpty</span><span>(</span><span>Mono</span><span>.</span><span>error</span><span>(</span><span>RuntimeException</span><span>(</span><span>"Todo not found"</span><span>)))</span> <span>// Handle missing Todo</span><span>.</span><span>map</span> <span>{</span> <span>existing</span> <span>-></span><span>existing</span><span>.</span><span>copy</span><span>(</span><span>title</span> <span>=</span> <span>request</span><span>.</span><span>title</span><span>,</span><span>description</span> <span>=</span> <span>request</span><span>.</span><span>description</span><span>,</span><span>completed</span> <span>=</span> <span>request</span><span>.</span><span>completed</span><span>,</span><span>dueDate</span> <span>=</span> <span>request</span><span>.</span><span>dueDate</span><span>?.</span><span>toInstant</span><span>(),</span><span>updatedAt</span> <span>=</span> <span>Instant</span><span>.</span><span>now</span><span>()</span> <span>// Update timestamp</span><span>)</span><span>}</span><span>.</span><span>flatMap</span> <span>{</span> <span>todoRepository</span><span>.</span><span>save</span><span>(</span><span>it</span><span>)</span> <span>}</span> <span>// Save updated Todo</span><span>.</span><span>map</span> <span>{</span> <span>TodoResponse</span><span>.</span><span>newBuilder</span><span>().</span><span>setTodo</span><span>(</span><span>it</span><span>.</span><span>toProto</span><span>()).</span><span>build</span><span>()</span> <span>}</span><span>.</span><span>subscribe</span><span>(</span><span>{</span> <span>response</span> <span>-></span> <span>responseObserver</span><span>.</span><span>onNext</span><span>(</span><span>response</span><span>);</span> <span>responseObserver</span><span>.</span><span>onCompleted</span><span>()</span> <span>},</span><span>{</span> <span>error</span> <span>-></span> <span>responseObserver</span><span>.</span><span>onError</span><span>(</span><span>error</span><span>)</span> <span>}</span><span>)</span><span>}</span><span>// gRPC method to delete a Todo item by its ID</span><span>override</span> <span>fun</span> <span>deleteTodo</span><span>(</span><span>request</span><span>:</span> <span>TodoIdRequest</span><span>?,</span> <span>responseObserver</span><span>:</span> <span>StreamObserver</span><span><</span><span>Empty</span><span>>?)</span> <span>{</span><span>request</span><span>?.</span><span>let</span> <span>{</span><span>todoRepository</span><span>.</span><span>deleteById</span><span>(</span><span>it</span><span>.</span><span>id</span><span>)</span><span>.</span><span>then</span><span>(</span><span>Mono</span><span>.</span><span>just</span><span>(</span><span>Empty</span><span>.</span><span>newBuilder</span><span>().</span><span>build</span><span>()))</span> <span>// Return an empty response</span><span>.</span><span>subscribe</span><span>(</span><span>{</span> <span>response</span> <span>-></span> <span>responseObserver</span><span>?.</span><span>onNext</span><span>(</span><span>response</span><span>);</span> <span>responseObserver</span><span>?.</span><span>onCompleted</span><span>()</span> <span>},</span><span>{</span> <span>error</span> <span>-></span> <span>responseObserver</span><span>?.</span><span>onError</span><span>(</span><span>error</span><span>)</span> <span>}</span><span>)</span><span>}</span> <span>?:</span> <span>run</span> <span>{</span><span>// If request is null, complete the response</span><span>responseObserver</span><span>?.</span><span>onNext</span><span>(</span><span>Empty</span><span>.</span><span>newBuilder</span><span>().</span><span>build</span><span>())</span><span>responseObserver</span><span>?.</span><span>onCompleted</span><span>()</span><span>}</span><span>}</span><span>}</span><span>// Extension function to convert a Todo entity to a gRPC TodoItem message</span><span>fun</span> <span>Todo</span><span>.</span><span>toProto</span><span>():</span> <span>TodoItem</span> <span>{</span><span>val</span> <span>builder</span> <span>=</span> <span>TodoItem</span><span>.</span><span>newBuilder</span><span>()</span><span>.</span><span>setId</span><span>(</span><span>id</span><span>!!</span><span>)</span><span>.</span><span>setTitle</span><span>(</span><span>title</span><span>)</span><span>.</span><span>setDescription</span><span>(</span><span>description</span><span>)</span><span>.</span><span>setCompleted</span><span>(</span><span>completed</span><span>)</span><span>.</span><span>setCreatedAt</span><span>(</span><span>Timestamp</span><span>.</span><span>newBuilder</span><span>()</span><span>.</span><span>setSeconds</span><span>(</span><span>createdAt</span><span>.</span><span>epochSecond</span><span>)</span><span>.</span><span>setNanos</span><span>(</span><span>createdAt</span><span>.</span><span>nano</span><span>)</span><span>.</span><span>build</span><span>())</span><span>.</span><span>setUpdatedAt</span><span>(</span><span>Timestamp</span><span>.</span><span>newBuilder</span><span>()</span><span>.</span><span>setSeconds</span><span>(</span><span>updatedAt</span><span>.</span><span>epochSecond</span><span>)</span><span>.</span><span>setNanos</span><span>(</span><span>updatedAt</span><span>.</span><span>nano</span><span>)</span><span>.</span><span>build</span><span>())</span><span>dueDate</span><span>?.</span><span>let</span> <span>{</span><span>builder</span><span>.</span><span>setDueDate</span><span>(</span><span>Timestamp</span><span>.</span><span>newBuilder</span><span>()</span><span>.</span><span>setSeconds</span><span>(</span><span>it</span><span>.</span><span>epochSecond</span><span>)</span><span>.</span><span>setNanos</span><span>(</span><span>it</span><span>.</span><span>nano</span><span>)</span><span>.</span><span>build</span><span>())</span><span>}</span><span>return</span> <span>builder</span><span>.</span><span>build</span><span>()</span><span>}</span><span>// Extension function to convert a gRPC Timestamp to Java Instant</span><span>fun</span> <span>Timestamp</span><span>.</span><span>toInstant</span><span>():</span> <span>Instant</span> <span>=</span> <span>Instant</span><span>.</span><span>ofEpochSecond</span><span>(</span><span>seconds</span><span>,</span> <span>nanos</span><span>.</span><span>toLong</span><span>())</span><span>package</span> <span>com.mahmud.grpcserver.service</span> <span>import</span> <span>com.mahmud.grpcserver.model.Todo</span> <span>import</span> <span>com.google.protobuf.Timestamp</span> <span>import</span> <span>com.mahmud.grpcserver.*</span> <span>import</span> <span>com.mahmud.grpcserver.repository.TodoRepository</span> <span>import</span> <span>io.grpc.stub.StreamObserver</span> <span>import</span> <span>org.springframework.beans.factory.annotation.Autowired</span> <span>import</span> <span>org.springframework.grpc.server.service.GrpcService</span> <span>import</span> <span>reactor.core.publisher.Mono</span> <span>import</span> <span>java.time.Instant</span> <span>@GrpcService</span> <span>class</span> <span>TodoService</span> <span>@Autowired</span> <span>constructor</span><span>(</span> <span>private</span> <span>val</span> <span>todoRepository</span><span>:</span> <span>TodoRepository</span> <span>)</span> <span>:</span> <span>TodoServiceGrpc</span><span>.</span><span>TodoServiceImplBase</span><span>()</span> <span>{</span> <span>// gRPC method to create a new Todo item</span> <span>override</span> <span>fun</span> <span>createTodo</span><span>(</span><span>request</span><span>:</span> <span>TodoRequest</span><span>,</span> <span>responseObserver</span><span>:</span> <span>StreamObserver</span><span><</span><span>TodoResponse</span><span>>)</span> <span>{</span> <span>val</span> <span>todo</span> <span>=</span> <span>Todo</span><span>(</span> <span>title</span> <span>=</span> <span>request</span><span>.</span><span>title</span><span>,</span> <span>description</span> <span>=</span> <span>request</span><span>.</span><span>description</span><span>,</span> <span>completed</span> <span>=</span> <span>request</span><span>.</span><span>completed</span><span>,</span> <span>dueDate</span> <span>=</span> <span>request</span><span>.</span><span>dueDate</span><span>?.</span><span>toInstant</span><span>()</span> <span>// Convert protobuf Timestamp to Java Instant</span> <span>)</span> <span>// Save Todo to database using reactive programming</span> <span>todoRepository</span><span>.</span><span>save</span><span>(</span><span>todo</span><span>)</span> <span>.</span><span>map</span> <span>{</span> <span>TodoResponse</span><span>.</span><span>newBuilder</span><span>().</span><span>setTodo</span><span>(</span><span>it</span><span>.</span><span>toProto</span><span>()).</span><span>build</span><span>()</span> <span>}</span> <span>.</span><span>subscribe</span><span>(</span> <span>{</span> <span>response</span> <span>-></span> <span>responseObserver</span><span>.</span><span>onNext</span><span>(</span><span>response</span><span>);</span> <span>responseObserver</span><span>.</span><span>onCompleted</span><span>()</span> <span>},</span> <span>{</span> <span>error</span> <span>-></span> <span>responseObserver</span><span>.</span><span>onError</span><span>(</span><span>error</span><span>)</span> <span>}</span> <span>)</span> <span>}</span> <span>// gRPC method to retrieve a Todo by its ID</span> <span>override</span> <span>fun</span> <span>getTodo</span><span>(</span><span>request</span><span>:</span> <span>TodoIdRequest</span><span>,</span> <span>responseObserver</span><span>:</span> <span>StreamObserver</span><span><</span><span>TodoResponse</span><span>>)</span> <span>{</span> <span>todoRepository</span><span>.</span><span>findById</span><span>(</span><span>request</span><span>.</span><span>id</span><span>)</span> <span>.</span><span>switchIfEmpty</span><span>(</span><span>Mono</span><span>.</span><span>error</span><span>(</span><span>RuntimeException</span><span>(</span><span>"Todo not found"</span><span>)))</span> <span>// Handle missing Todo</span> <span>.</span><span>map</span> <span>{</span> <span>TodoResponse</span><span>.</span><span>newBuilder</span><span>().</span><span>setTodo</span><span>(</span><span>it</span><span>.</span><span>toProto</span><span>()).</span><span>build</span><span>()</span> <span>}</span> <span>.</span><span>subscribe</span><span>(</span> <span>{</span> <span>response</span> <span>-></span> <span>responseObserver</span><span>.</span><span>onNext</span><span>(</span><span>response</span><span>);</span> <span>responseObserver</span><span>.</span><span>onCompleted</span><span>()</span> <span>},</span> <span>{</span> <span>error</span> <span>-></span> <span>responseObserver</span><span>.</span><span>onError</span><span>(</span><span>error</span><span>)</span> <span>}</span> <span>)</span> <span>}</span> <span>// gRPC method to list all Todos with an optional filter for completed ones</span> <span>override</span> <span>fun</span> <span>listTodos</span><span>(</span><span>request</span><span>:</span> <span>ListTodosRequest</span><span>,</span> <span>responseObserver</span><span>:</span> <span>StreamObserver</span><span><</span><span>ListTodosResponse</span><span>>)</span> <span>{</span> <span>val</span> <span>todos</span> <span>=</span> <span>if</span> <span>(</span><span>request</span><span>.</span><span>showCompleted</span><span>)</span> <span>{</span> <span>todoRepository</span><span>.</span><span>findAll</span><span>()</span> <span>// Fetch all Todos</span> <span>}</span> <span>else</span> <span>{</span> <span>todoRepository</span><span>.</span><span>findByCompleted</span><span>(</span><span>false</span><span>)</span> <span>// Fetch only incomplete Todos</span> <span>}</span> <span>todos</span><span>.</span><span>collectList</span><span>()</span> <span>.</span><span>map</span> <span>{</span> <span>items</span> <span>-></span> <span>ListTodosResponse</span><span>.</span><span>newBuilder</span><span>()</span> <span>.</span><span>addAllTodos</span><span>(</span><span>items</span><span>.</span><span>map</span> <span>{</span> <span>it</span><span>.</span><span>toProto</span><span>()</span> <span>})</span> <span>// Convert to protobuf format</span> <span>.</span><span>build</span><span>()</span> <span>}</span> <span>.</span><span>subscribe</span><span>(</span> <span>{</span> <span>response</span> <span>-></span> <span>responseObserver</span><span>.</span><span>onNext</span><span>(</span><span>response</span><span>);</span> <span>responseObserver</span><span>.</span><span>onCompleted</span><span>()</span> <span>},</span> <span>{</span> <span>error</span> <span>-></span> <span>responseObserver</span><span>.</span><span>onError</span><span>(</span><span>error</span><span>)</span> <span>}</span> <span>)</span> <span>}</span> <span>// gRPC method to update an existing Todo item</span> <span>override</span> <span>fun</span> <span>updateTodo</span><span>(</span><span>request</span><span>:</span> <span>TodoRequest</span><span>,</span> <span>responseObserver</span><span>:</span> <span>StreamObserver</span><span><</span><span>TodoResponse</span><span>>)</span> <span>{</span> <span>todoRepository</span><span>.</span><span>findById</span><span>(</span><span>request</span><span>.</span><span>id</span><span>)</span> <span>.</span><span>switchIfEmpty</span><span>(</span><span>Mono</span><span>.</span><span>error</span><span>(</span><span>RuntimeException</span><span>(</span><span>"Todo not found"</span><span>)))</span> <span>// Handle missing Todo</span> <span>.</span><span>map</span> <span>{</span> <span>existing</span> <span>-></span> <span>existing</span><span>.</span><span>copy</span><span>(</span> <span>title</span> <span>=</span> <span>request</span><span>.</span><span>title</span><span>,</span> <span>description</span> <span>=</span> <span>request</span><span>.</span><span>description</span><span>,</span> <span>completed</span> <span>=</span> <span>request</span><span>.</span><span>completed</span><span>,</span> <span>dueDate</span> <span>=</span> <span>request</span><span>.</span><span>dueDate</span><span>?.</span><span>toInstant</span><span>(),</span> <span>updatedAt</span> <span>=</span> <span>Instant</span><span>.</span><span>now</span><span>()</span> <span>// Update timestamp</span> <span>)</span> <span>}</span> <span>.</span><span>flatMap</span> <span>{</span> <span>todoRepository</span><span>.</span><span>save</span><span>(</span><span>it</span><span>)</span> <span>}</span> <span>// Save updated Todo</span> <span>.</span><span>map</span> <span>{</span> <span>TodoResponse</span><span>.</span><span>newBuilder</span><span>().</span><span>setTodo</span><span>(</span><span>it</span><span>.</span><span>toProto</span><span>()).</span><span>build</span><span>()</span> <span>}</span> <span>.</span><span>subscribe</span><span>(</span> <span>{</span> <span>response</span> <span>-></span> <span>responseObserver</span><span>.</span><span>onNext</span><span>(</span><span>response</span><span>);</span> <span>responseObserver</span><span>.</span><span>onCompleted</span><span>()</span> <span>},</span> <span>{</span> <span>error</span> <span>-></span> <span>responseObserver</span><span>.</span><span>onError</span><span>(</span><span>error</span><span>)</span> <span>}</span> <span>)</span> <span>}</span> <span>// gRPC method to delete a Todo item by its ID</span> <span>override</span> <span>fun</span> <span>deleteTodo</span><span>(</span><span>request</span><span>:</span> <span>TodoIdRequest</span><span>?,</span> <span>responseObserver</span><span>:</span> <span>StreamObserver</span><span><</span><span>Empty</span><span>>?)</span> <span>{</span> <span>request</span><span>?.</span><span>let</span> <span>{</span> <span>todoRepository</span><span>.</span><span>deleteById</span><span>(</span><span>it</span><span>.</span><span>id</span><span>)</span> <span>.</span><span>then</span><span>(</span><span>Mono</span><span>.</span><span>just</span><span>(</span><span>Empty</span><span>.</span><span>newBuilder</span><span>().</span><span>build</span><span>()))</span> <span>// Return an empty response</span> <span>.</span><span>subscribe</span><span>(</span> <span>{</span> <span>response</span> <span>-></span> <span>responseObserver</span><span>?.</span><span>onNext</span><span>(</span><span>response</span><span>);</span> <span>responseObserver</span><span>?.</span><span>onCompleted</span><span>()</span> <span>},</span> <span>{</span> <span>error</span> <span>-></span> <span>responseObserver</span><span>?.</span><span>onError</span><span>(</span><span>error</span><span>)</span> <span>}</span> <span>)</span> <span>}</span> <span>?:</span> <span>run</span> <span>{</span> <span>// If request is null, complete the response</span> <span>responseObserver</span><span>?.</span><span>onNext</span><span>(</span><span>Empty</span><span>.</span><span>newBuilder</span><span>().</span><span>build</span><span>())</span> <span>responseObserver</span><span>?.</span><span>onCompleted</span><span>()</span> <span>}</span> <span>}</span> <span>}</span> <span>// Extension function to convert a Todo entity to a gRPC TodoItem message</span> <span>fun</span> <span>Todo</span><span>.</span><span>toProto</span><span>():</span> <span>TodoItem</span> <span>{</span> <span>val</span> <span>builder</span> <span>=</span> <span>TodoItem</span><span>.</span><span>newBuilder</span><span>()</span> <span>.</span><span>setId</span><span>(</span><span>id</span><span>!!</span><span>)</span> <span>.</span><span>setTitle</span><span>(</span><span>title</span><span>)</span> <span>.</span><span>setDescription</span><span>(</span><span>description</span><span>)</span> <span>.</span><span>setCompleted</span><span>(</span><span>completed</span><span>)</span> <span>.</span><span>setCreatedAt</span><span>(</span><span>Timestamp</span><span>.</span><span>newBuilder</span><span>()</span> <span>.</span><span>setSeconds</span><span>(</span><span>createdAt</span><span>.</span><span>epochSecond</span><span>)</span> <span>.</span><span>setNanos</span><span>(</span><span>createdAt</span><span>.</span><span>nano</span><span>)</span> <span>.</span><span>build</span><span>())</span> <span>.</span><span>setUpdatedAt</span><span>(</span><span>Timestamp</span><span>.</span><span>newBuilder</span><span>()</span> <span>.</span><span>setSeconds</span><span>(</span><span>updatedAt</span><span>.</span><span>epochSecond</span><span>)</span> <span>.</span><span>setNanos</span><span>(</span><span>updatedAt</span><span>.</span><span>nano</span><span>)</span> <span>.</span><span>build</span><span>())</span> <span>dueDate</span><span>?.</span><span>let</span> <span>{</span> <span>builder</span><span>.</span><span>setDueDate</span><span>(</span><span>Timestamp</span><span>.</span><span>newBuilder</span><span>()</span> <span>.</span><span>setSeconds</span><span>(</span><span>it</span><span>.</span><span>epochSecond</span><span>)</span> <span>.</span><span>setNanos</span><span>(</span><span>it</span><span>.</span><span>nano</span><span>)</span> <span>.</span><span>build</span><span>())</span> <span>}</span> <span>return</span> <span>builder</span><span>.</span><span>build</span><span>()</span> <span>}</span> <span>// Extension function to convert a gRPC Timestamp to Java Instant</span> <span>fun</span> <span>Timestamp</span><span>.</span><span>toInstant</span><span>():</span> <span>Instant</span> <span>=</span> <span>Instant</span><span>.</span><span>ofEpochSecond</span><span>(</span><span>seconds</span><span>,</span> <span>nanos</span><span>.</span><span>toLong</span><span>())</span>package com.mahmud.grpcserver.service import com.mahmud.grpcserver.model.Todo import com.google.protobuf.Timestamp import com.mahmud.grpcserver.* import com.mahmud.grpcserver.repository.TodoRepository import io.grpc.stub.StreamObserver import org.springframework.beans.factory.annotation.Autowired import org.springframework.grpc.server.service.GrpcService import reactor.core.publisher.Mono import java.time.Instant @GrpcService class TodoService @Autowired constructor( private val todoRepository: TodoRepository ) : TodoServiceGrpc.TodoServiceImplBase() { // gRPC method to create a new Todo item override fun createTodo(request: TodoRequest, responseObserver: StreamObserver<TodoResponse>) { val todo = Todo( title = request.title, description = request.description, completed = request.completed, dueDate = request.dueDate?.toInstant() // Convert protobuf Timestamp to Java Instant ) // Save Todo to database using reactive programming todoRepository.save(todo) .map { TodoResponse.newBuilder().setTodo(it.toProto()).build() } .subscribe( { response -> responseObserver.onNext(response); responseObserver.onCompleted() }, { error -> responseObserver.onError(error) } ) } // gRPC method to retrieve a Todo by its ID override fun getTodo(request: TodoIdRequest, responseObserver: StreamObserver<TodoResponse>) { todoRepository.findById(request.id) .switchIfEmpty(Mono.error(RuntimeException("Todo not found"))) // Handle missing Todo .map { TodoResponse.newBuilder().setTodo(it.toProto()).build() } .subscribe( { response -> responseObserver.onNext(response); responseObserver.onCompleted() }, { error -> responseObserver.onError(error) } ) } // gRPC method to list all Todos with an optional filter for completed ones override fun listTodos(request: ListTodosRequest, responseObserver: StreamObserver<ListTodosResponse>) { val todos = if (request.showCompleted) { todoRepository.findAll() // Fetch all Todos } else { todoRepository.findByCompleted(false) // Fetch only incomplete Todos } todos.collectList() .map { items -> ListTodosResponse.newBuilder() .addAllTodos(items.map { it.toProto() }) // Convert to protobuf format .build() } .subscribe( { response -> responseObserver.onNext(response); responseObserver.onCompleted() }, { error -> responseObserver.onError(error) } ) } // gRPC method to update an existing Todo item override fun updateTodo(request: TodoRequest, responseObserver: StreamObserver<TodoResponse>) { todoRepository.findById(request.id) .switchIfEmpty(Mono.error(RuntimeException("Todo not found"))) // Handle missing Todo .map { existing -> existing.copy( title = request.title, description = request.description, completed = request.completed, dueDate = request.dueDate?.toInstant(), updatedAt = Instant.now() // Update timestamp ) } .flatMap { todoRepository.save(it) } // Save updated Todo .map { TodoResponse.newBuilder().setTodo(it.toProto()).build() } .subscribe( { response -> responseObserver.onNext(response); responseObserver.onCompleted() }, { error -> responseObserver.onError(error) } ) } // gRPC method to delete a Todo item by its ID override fun deleteTodo(request: TodoIdRequest?, responseObserver: StreamObserver<Empty>?) { request?.let { todoRepository.deleteById(it.id) .then(Mono.just(Empty.newBuilder().build())) // Return an empty response .subscribe( { response -> responseObserver?.onNext(response); responseObserver?.onCompleted() }, { error -> responseObserver?.onError(error) } ) } ?: run { // If request is null, complete the response responseObserver?.onNext(Empty.newBuilder().build()) responseObserver?.onCompleted() } } } // Extension function to convert a Todo entity to a gRPC TodoItem message fun Todo.toProto(): TodoItem { val builder = TodoItem.newBuilder() .setId(id!!) .setTitle(title) .setDescription(description) .setCompleted(completed) .setCreatedAt(Timestamp.newBuilder() .setSeconds(createdAt.epochSecond) .setNanos(createdAt.nano) .build()) .setUpdatedAt(Timestamp.newBuilder() .setSeconds(updatedAt.epochSecond) .setNanos(updatedAt.nano) .build()) dueDate?.let { builder.setDueDate(Timestamp.newBuilder() .setSeconds(it.epochSecond) .setNanos(it.nano) .build()) } return builder.build() } // Extension function to convert a gRPC Timestamp to Java Instant fun Timestamp.toInstant(): Instant = Instant.ofEpochSecond(seconds, nanos.toLong())
Enter fullscreen mode Exit fullscreen mode
Breakdown:
-
@GrpcService
: Integrates with Spring gRPC. - Comments explain gRPC request handling, response building, and call completion.
Step 5: Run the Server
Ensure MongoDB is running.
Run:
./gradlew bootRun./gradlew bootRun./gradlew bootRun
Enter fullscreen mode Exit fullscreen mode
The gRPC server listens on localhost:9090
.
Let’s test the gGRPC Server using grpcurl
grpcurl
is a command-line tool for interacting with gRPC servers. These commands assume the server supports reflection (common in development) or that you have the todo.proto
file locally. If reflection isn’t enabled, add -proto todo.proto
to each command and ensure the file is in your working directory.
1. Create Todo
Creates a new Todo item.
grpcurl <span>-plaintext</span> <span>-d</span> <span>'{ "title": "Buy groceries", "description": "Milk, bread, eggs", "completed": false, "due_date": {"seconds": 1735689600, "nanos": 0} }'</span> localhost:9090 todo.TodoService/CreateTodogrpcurl <span>-plaintext</span> <span>-d</span> <span>'{ "title": "Buy groceries", "description": "Milk, bread, eggs", "completed": false, "due_date": {"seconds": 1735689600, "nanos": 0} }'</span> localhost:9090 todo.TodoService/CreateTodogrpcurl -plaintext -d '{ "title": "Buy groceries", "description": "Milk, bread, eggs", "completed": false, "due_date": {"seconds": 1735689600, "nanos": 0} }' localhost:9090 todo.TodoService/CreateTodo
Enter fullscreen mode Exit fullscreen mode
-
-plaintext
: No TLS (matches server config). -
-d
: JSON payload for theTodoRequest
message. -
due_date
: Set to a future timestamp (e.g., Dec 31, 2025, 00:00:00 UTC). - Expected response:
TodoResponse
with the createdTodoItem
.
2. Get Todo
Retrieves a Todo by ID (replace <todo-id>
with an actual ID from a prior CreateTodo
response).
grpcurl <span>-plaintext</span> <span>-d</span> <span>'{"id": "<todo-id>"}'</span> localhost:9090 todo.TodoService/GetTodogrpcurl <span>-plaintext</span> <span>-d</span> <span>'{"id": "<todo-id>"}'</span> localhost:9090 todo.TodoService/GetTodogrpcurl -plaintext -d '{"id": "<todo-id>"}' localhost:9090 todo.TodoService/GetTodo
Enter fullscreen mode Exit fullscreen mode
- Payload:
TodoIdRequest
with the Todo’s ID. - Expected response:
TodoResponse
with the matchingTodoItem
.
3. List Todos
Lists all Todo items, optionally filtering by completion status.
grpcurl <span>-plaintext</span> <span>-d</span> <span>'{"show_completed": true, "page_size": 10}'</span> localhost:9090 todo.TodoService/ListTodosgrpcurl <span>-plaintext</span> <span>-d</span> <span>'{"show_completed": true, "page_size": 10}'</span> localhost:9090 todo.TodoService/ListTodosgrpcurl -plaintext -d '{"show_completed": true, "page_size": 10}' localhost:9090 todo.TodoService/ListTodos
Enter fullscreen mode Exit fullscreen mode
-
show_completed
:true
to include completed Todos; set tofalse
to filter them out. -
page_size
: Limits results (e.g., 10 items). - Expected response:
ListTodosResponse
with a list ofTodoItem
s.
4. Update Todo
Updates an existing Todo (replace <todo-id>
with an actual ID).
grpcurl <span>-plaintext</span> <span>-d</span> <span>'{ "id": "<todo-id>", "title": "Buy groceries updated", "description": "Milk, bread, eggs, cheese", "completed": true, "due_date": {"seconds": 1735776000, "nanos": 0} }'</span> localhost:9090 todo.TodoService/UpdateTodogrpcurl <span>-plaintext</span> <span>-d</span> <span>'{ "id": "<todo-id>", "title": "Buy groceries updated", "description": "Milk, bread, eggs, cheese", "completed": true, "due_date": {"seconds": 1735776000, "nanos": 0} }'</span> localhost:9090 todo.TodoService/UpdateTodogrpcurl -plaintext -d '{ "id": "<todo-id>", "title": "Buy groceries updated", "description": "Milk, bread, eggs, cheese", "completed": true, "due_date": {"seconds": 1735776000, "nanos": 0} }' localhost:9090 todo.TodoService/UpdateTodo
Enter fullscreen mode Exit fullscreen mode
- Payload:
TodoRequest
with updated fields. -
due_date
: Updated to Jan 1, 2026, 00:00:00 UTC. - Expected response:
TodoResponse
with the updatedTodoItem
.
5. Delete Todo
Deletes a Todo by ID (replace <todo-id>
with an actual ID).
grpcurl <span>-plaintext</span> <span>-d</span> <span>'{"id": "<todo-id>"}'</span> localhost:9090 todo.TodoService/DeleteTodogrpcurl <span>-plaintext</span> <span>-d</span> <span>'{"id": "<todo-id>"}'</span> localhost:9090 todo.TodoService/DeleteTodogrpcurl -plaintext -d '{"id": "<todo-id>"}' localhost:9090 todo.TodoService/DeleteTodo
Enter fullscreen mode Exit fullscreen mode
- Payload:
TodoIdRequest
with the Todo’s ID. - Expected response: Empty message (
{}
).
gRPC Client Implementation
Now, let’s build the grpc-client
to expose REST endpoints and interact with the server.
Step 1: Project Setup
-
Create the Project:
- Use Spring Initializr to generate a Spring Boot 3 project with Kotlin.
- In my case, I have set the group to
com.mahmud
, artifact togrpc-client
.
-
Add Dependencies:
- Include the following dependencies:
<span>org</span><span>.</span><span>springframework</span><span>.</span><span>boot</span><span>:</span><span>spring-boot-starter-webflux</span><span>org</span><span>.</span><span>springframework</span><span>.</span><span>grpc</span><span>:</span><span>spring-grpc-spring-boot-starter</span><span>org</span><span>.</span><span>springframework</span><span>.</span><span>boot</span><span>:</span><span>spring-boot-starter-webflux</span> <span>org</span><span>.</span><span>springframework</span><span>.</span><span>grpc</span><span>:</span><span>spring-grpc-spring-boot-starter</span>
org.springframework.boot:spring-boot-starter-webflux org.springframework.grpc:spring-grpc-spring-boot-starter
-
Configure Application:
- Create
application.yaml
:
<span>spring</span><span>:</span><span>application</span><span>:</span><span>name</span><span>:</span> <span>grpc-client</span><span>grpc</span><span>:</span><span>server</span><span>:</span><span>port</span><span>:</span> <span>9091</span><span>spring</span><span>:</span> <span>application</span><span>:</span> <span>name</span><span>:</span> <span>grpc-client</span> <span>grpc</span><span>:</span> <span>server</span><span>:</span> <span>port</span><span>:</span> <span>9091</span>
spring: application: name: grpc-client grpc: server: port: 9091
- Create
Step 2: Define the Protobuf File
Create todo.proto
file in src/main/proto
directory. It defines the client’s TodoService
, matching the server’s contract with a different package. Identical to the server’s todo.proto
except for java_package
.
<span>syntax</span> <span>=</span> <span>"proto3"</span><span>;</span><span>package</span> <span>todo</span><span>;</span><span>option</span> <span>java_multiple_files</span> <span>=</span> <span>true</span><span>;</span><span>option</span> <span>java_package</span> <span>=</span> <span>"com.mahmud.grpcclient"</span><span>;</span><span>option</span> <span>java_outer_classname</span> <span>=</span> <span>"TodoProto"</span><span>;</span><span>service</span> <span>TodoService</span> <span>{</span><span>rpc</span> <span>CreateTodo</span> <span>(</span><span>TodoRequest</span><span>)</span> <span>returns</span> <span>(</span><span>TodoResponse</span><span>)</span> <span>{}</span><span>rpc</span> <span>GetTodo</span> <span>(</span><span>TodoIdRequest</span><span>)</span> <span>returns</span> <span>(</span><span>TodoResponse</span><span>)</span> <span>{}</span><span>rpc</span> <span>ListTodos</span> <span>(</span><span>ListTodosRequest</span><span>)</span> <span>returns</span> <span>(</span><span>ListTodosResponse</span><span>)</span> <span>{}</span><span>rpc</span> <span>UpdateTodo</span> <span>(</span><span>TodoRequest</span><span>)</span> <span>returns</span> <span>(</span><span>TodoResponse</span><span>)</span> <span>{}</span><span>rpc</span> <span>DeleteTodo</span> <span>(</span><span>TodoIdRequest</span><span>)</span> <span>returns</span> <span>(</span><span>Empty</span><span>)</span> <span>{}</span><span>}</span><span>message</span> <span>TodoItem</span> <span>{</span><span>string</span> <span>id</span> <span>=</span> <span>1</span><span>;</span><span>string</span> <span>title</span> <span>=</span> <span>2</span><span>;</span><span>string</span> <span>description</span> <span>=</span> <span>3</span><span>;</span><span>bool</span> <span>completed</span> <span>=</span> <span>4</span><span>;</span><span>google.protobuf.Timestamp</span> <span>created_at</span> <span>=</span> <span>5</span><span>;</span><span>google.protobuf.Timestamp</span> <span>updated_at</span> <span>=</span> <span>6</span><span>;</span><span>optional</span> <span>google.protobuf.Timestamp</span> <span>due_date</span> <span>=</span> <span>7</span><span>;</span><span>}</span><span>message</span> <span>TodoRequest</span> <span>{</span><span>string</span> <span>id</span> <span>=</span> <span>1</span><span>;</span><span>string</span> <span>title</span> <span>=</span> <span>2</span><span>;</span><span>string</span> <span>description</span> <span>=</span> <span>3</span><span>;</span><span>bool</span> <span>completed</span> <span>=</span> <span>4</span><span>;</span><span>optional</span> <span>google.protobuf.Timestamp</span> <span>due_date</span> <span>=</span> <span>5</span><span>;</span><span>}</span><span>message</span> <span>TodoResponse</span> <span>{</span><span>TodoItem</span> <span>todo</span> <span>=</span> <span>1</span><span>;</span><span>}</span><span>message</span> <span>TodoIdRequest</span> <span>{</span><span>string</span> <span>id</span> <span>=</span> <span>1</span><span>;</span><span>}</span><span>message</span> <span>ListTodosRequest</span> <span>{</span><span>optional</span> <span>bool</span> <span>show_completed</span> <span>=</span> <span>1</span><span>;</span><span>int32</span> <span>page_size</span> <span>=</span> <span>2</span><span>;</span><span>string</span> <span>page_token</span> <span>=</span> <span>3</span><span>;</span><span>}</span><span>message</span> <span>ListTodosResponse</span> <span>{</span><span>repeated</span> <span>TodoItem</span> <span>todos</span> <span>=</span> <span>1</span><span>;</span><span>string</span> <span>next_page_token</span> <span>=</span> <span>2</span><span>;</span><span>}</span><span>message</span> <span>Empty</span> <span>{}</span><span>import</span> <span>"google/protobuf/timestamp.proto"</span><span>;</span><span>syntax</span> <span>=</span> <span>"proto3"</span><span>;</span> <span>package</span> <span>todo</span><span>;</span> <span>option</span> <span>java_multiple_files</span> <span>=</span> <span>true</span><span>;</span> <span>option</span> <span>java_package</span> <span>=</span> <span>"com.mahmud.grpcclient"</span><span>;</span> <span>option</span> <span>java_outer_classname</span> <span>=</span> <span>"TodoProto"</span><span>;</span> <span>service</span> <span>TodoService</span> <span>{</span> <span>rpc</span> <span>CreateTodo</span> <span>(</span><span>TodoRequest</span><span>)</span> <span>returns</span> <span>(</span><span>TodoResponse</span><span>)</span> <span>{}</span> <span>rpc</span> <span>GetTodo</span> <span>(</span><span>TodoIdRequest</span><span>)</span> <span>returns</span> <span>(</span><span>TodoResponse</span><span>)</span> <span>{}</span> <span>rpc</span> <span>ListTodos</span> <span>(</span><span>ListTodosRequest</span><span>)</span> <span>returns</span> <span>(</span><span>ListTodosResponse</span><span>)</span> <span>{}</span> <span>rpc</span> <span>UpdateTodo</span> <span>(</span><span>TodoRequest</span><span>)</span> <span>returns</span> <span>(</span><span>TodoResponse</span><span>)</span> <span>{}</span> <span>rpc</span> <span>DeleteTodo</span> <span>(</span><span>TodoIdRequest</span><span>)</span> <span>returns</span> <span>(</span><span>Empty</span><span>)</span> <span>{}</span> <span>}</span> <span>message</span> <span>TodoItem</span> <span>{</span> <span>string</span> <span>id</span> <span>=</span> <span>1</span><span>;</span> <span>string</span> <span>title</span> <span>=</span> <span>2</span><span>;</span> <span>string</span> <span>description</span> <span>=</span> <span>3</span><span>;</span> <span>bool</span> <span>completed</span> <span>=</span> <span>4</span><span>;</span> <span>google.protobuf.Timestamp</span> <span>created_at</span> <span>=</span> <span>5</span><span>;</span> <span>google.protobuf.Timestamp</span> <span>updated_at</span> <span>=</span> <span>6</span><span>;</span> <span>optional</span> <span>google.protobuf.Timestamp</span> <span>due_date</span> <span>=</span> <span>7</span><span>;</span> <span>}</span> <span>message</span> <span>TodoRequest</span> <span>{</span> <span>string</span> <span>id</span> <span>=</span> <span>1</span><span>;</span> <span>string</span> <span>title</span> <span>=</span> <span>2</span><span>;</span> <span>string</span> <span>description</span> <span>=</span> <span>3</span><span>;</span> <span>bool</span> <span>completed</span> <span>=</span> <span>4</span><span>;</span> <span>optional</span> <span>google.protobuf.Timestamp</span> <span>due_date</span> <span>=</span> <span>5</span><span>;</span> <span>}</span> <span>message</span> <span>TodoResponse</span> <span>{</span> <span>TodoItem</span> <span>todo</span> <span>=</span> <span>1</span><span>;</span> <span>}</span> <span>message</span> <span>TodoIdRequest</span> <span>{</span> <span>string</span> <span>id</span> <span>=</span> <span>1</span><span>;</span> <span>}</span> <span>message</span> <span>ListTodosRequest</span> <span>{</span> <span>optional</span> <span>bool</span> <span>show_completed</span> <span>=</span> <span>1</span><span>;</span> <span>int32</span> <span>page_size</span> <span>=</span> <span>2</span><span>;</span> <span>string</span> <span>page_token</span> <span>=</span> <span>3</span><span>;</span> <span>}</span> <span>message</span> <span>ListTodosResponse</span> <span>{</span> <span>repeated</span> <span>TodoItem</span> <span>todos</span> <span>=</span> <span>1</span><span>;</span> <span>string</span> <span>next_page_token</span> <span>=</span> <span>2</span><span>;</span> <span>}</span> <span>message</span> <span>Empty</span> <span>{}</span> <span>import</span> <span>"google/protobuf/timestamp.proto"</span><span>;</span>syntax = "proto3"; package todo; option java_multiple_files = true; option java_package = "com.mahmud.grpcclient"; option java_outer_classname = "TodoProto"; service TodoService { rpc CreateTodo (TodoRequest) returns (TodoResponse) {} rpc GetTodo (TodoIdRequest) returns (TodoResponse) {} rpc ListTodos (ListTodosRequest) returns (ListTodosResponse) {} rpc UpdateTodo (TodoRequest) returns (TodoResponse) {} rpc DeleteTodo (TodoIdRequest) returns (Empty) {} } message TodoItem { string id = 1; string title = 2; string description = 3; bool completed = 4; google.protobuf.Timestamp created_at = 5; google.protobuf.Timestamp updated_at = 6; optional google.protobuf.Timestamp due_date = 7; } message TodoRequest { string id = 1; string title = 2; string description = 3; bool completed = 4; optional google.protobuf.Timestamp due_date = 5; } message TodoResponse { TodoItem todo = 1; } message TodoIdRequest { string id = 1; } message ListTodosRequest { optional bool show_completed = 1; int32 page_size = 2; string page_token = 3; } message ListTodosResponse { repeated TodoItem todos = 1; string next_page_token = 2; } message Empty {} import "google/protobuf/timestamp.proto";
Enter fullscreen mode Exit fullscreen mode
Step 3: Generate gRPC Stubs
Run:
./gradlew build./gradlew build./gradlew build
Enter fullscreen mode Exit fullscreen mode
This generates stubs in build/generated/source/proto/main
. Mark as a generated source root in your IDE.
Step 4: Implement Client Logic
Let’s configure the gRPC client according to the grpc-server.
GrpcClientConfig.kt
<span>package</span> <span>com.mahmud.grpcclient.config</span><span>import</span> <span>com.mahmud.grpcclient.TodoServiceGrpc</span><span>import</span> <span>io.grpc.ManagedChannel</span><span>import</span> <span>io.grpc.ManagedChannelBuilder</span><span>import</span> <span>org.springframework.context.annotation.Bean</span><span>import</span> <span>org.springframework.context.annotation.Configuration</span><span>@Configuration</span><span>class</span> <span>GrpcClientConfig</span> <span>{</span><span>@Bean</span><span>fun</span> <span>todoServiceStub</span><span>():</span> <span>TodoServiceGrpc</span><span>.</span><span>TodoServiceBlockingStub</span> <span>{</span><span>val</span> <span>channel</span><span>:</span> <span>ManagedChannel</span> <span>=</span> <span>ManagedChannelBuilder</span><span>.</span><span>forAddress</span><span>(</span><span>"localhost"</span><span>,</span> <span>9090</span><span>)</span> <span>// Connects to our gRPC server</span><span>.</span><span>usePlaintext</span><span>()</span> <span>// No TLS for simplicity</span><span>.</span><span>build</span><span>()</span><span>return</span> <span>TodoServiceGrpc</span><span>.</span><span>newBlockingStub</span><span>(</span><span>channel</span><span>)</span><span>}</span><span>}</span><span>package</span> <span>com.mahmud.grpcclient.config</span> <span>import</span> <span>com.mahmud.grpcclient.TodoServiceGrpc</span> <span>import</span> <span>io.grpc.ManagedChannel</span> <span>import</span> <span>io.grpc.ManagedChannelBuilder</span> <span>import</span> <span>org.springframework.context.annotation.Bean</span> <span>import</span> <span>org.springframework.context.annotation.Configuration</span> <span>@Configuration</span> <span>class</span> <span>GrpcClientConfig</span> <span>{</span> <span>@Bean</span> <span>fun</span> <span>todoServiceStub</span><span>():</span> <span>TodoServiceGrpc</span><span>.</span><span>TodoServiceBlockingStub</span> <span>{</span> <span>val</span> <span>channel</span><span>:</span> <span>ManagedChannel</span> <span>=</span> <span>ManagedChannelBuilder</span> <span>.</span><span>forAddress</span><span>(</span><span>"localhost"</span><span>,</span> <span>9090</span><span>)</span> <span>// Connects to our gRPC server</span> <span>.</span><span>usePlaintext</span><span>()</span> <span>// No TLS for simplicity</span> <span>.</span><span>build</span><span>()</span> <span>return</span> <span>TodoServiceGrpc</span><span>.</span><span>newBlockingStub</span><span>(</span><span>channel</span><span>)</span> <span>}</span> <span>}</span>package com.mahmud.grpcclient.config import com.mahmud.grpcclient.TodoServiceGrpc import io.grpc.ManagedChannel import io.grpc.ManagedChannelBuilder import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration @Configuration class GrpcClientConfig { @Bean fun todoServiceStub(): TodoServiceGrpc.TodoServiceBlockingStub { val channel: ManagedChannel = ManagedChannelBuilder .forAddress("localhost", 9090) // Connects to our gRPC server .usePlaintext() // No TLS for simplicity .build() return TodoServiceGrpc.newBlockingStub(channel) } }
Enter fullscreen mode Exit fullscreen mode
Breakdown:
- We just set up a stub for server communication.
Implement the gRPC calls
The TodoClientService
handles gRPC calls by communicating with the server.
TodoClientService.kt
<span>package</span> <span>com.mahmud.grpcclient.service</span><span>import</span> <span>com.google.protobuf.Timestamp</span><span>import</span> <span>com.mahmud.grpcclient.*</span><span>import</span> <span>org.springframework.stereotype.Service</span><span>import</span> <span>java.time.Instant</span><span>// Spring service managing gRPC client calls to the server</span><span>@Service</span><span>class</span> <span>TodoClientService</span><span>(</span><span>private</span> <span>val</span> <span>todoStub</span><span>:</span> <span>TodoServiceGrpc</span><span>.</span><span>TodoServiceBlockingStub</span><span>)</span> <span>{</span><span>// gRPC call to create a new Todo on the server</span><span>fun</span> <span>createTodo</span><span>(</span><span>title</span><span>:</span> <span>String</span><span>,</span> <span>description</span><span>:</span> <span>String</span><span>,</span> <span>completed</span><span>:</span> <span>Boolean</span><span>,</span> <span>dueDate</span><span>:</span> <span>Instant</span><span>?):</span> <span>TodoItem</span><span>?</span> <span>{</span><span>// Build gRPC request from method parameters</span><span>val</span> <span>request</span> <span>=</span> <span>TodoRequest</span><span>.</span><span>newBuilder</span><span>()</span><span>.</span><span>setTitle</span><span>(</span><span>title</span><span>)</span><span>.</span><span>setDescription</span><span>(</span><span>description</span><span>)</span><span>.</span><span>setCompleted</span><span>(</span><span>completed</span><span>)</span><span>.</span><span>apply</span> <span>{</span><span>dueDate</span><span>?.</span><span>let</span> <span>{</span><span>setDueDate</span><span>(</span><span>Timestamp</span><span>.</span><span>newBuilder</span><span>()</span><span>.</span><span>setSeconds</span><span>(</span><span>it</span><span>.</span><span>epochSecond</span><span>)</span><span>.</span><span>setNanos</span><span>(</span><span>it</span><span>.</span><span>nano</span><span>)</span><span>.</span><span>build</span><span>())</span><span>}</span><span>}</span><span>.</span><span>build</span><span>()</span><span>// Invoke gRPC server method and return response</span><span>return</span> <span>todoStub</span><span>.</span><span>createTodo</span><span>(</span><span>request</span><span>).</span><span>todo</span><span>;</span><span>}</span><span>// gRPC call to retrieve a Todo by its ID from the server</span><span>fun</span> <span>getTodo</span><span>(</span><span>id</span><span>:</span> <span>String</span><span>):</span> <span>TodoItem</span> <span>{</span><span>// Construct gRPC request with Todo ID</span><span>val</span> <span>request</span> <span>=</span> <span>TodoIdRequest</span><span>.</span><span>newBuilder</span><span>().</span><span>setId</span><span>(</span><span>id</span><span>).</span><span>build</span><span>()</span><span>// Fetch Todo from gRPC server and return it</span><span>return</span> <span>todoStub</span><span>.</span><span>getTodo</span><span>(</span><span>request</span><span>).</span><span>todo</span><span>}</span><span>// gRPC call to list Todos, optionally showing completed ones</span><span>fun</span> <span>listTodos</span><span>(</span><span>showCompleted</span><span>:</span> <span>Boolean</span><span>):</span> <span>MutableList</span><span><</span><span>TodoItem</span><span>>?</span> <span>{</span><span>// Create gRPC request with filter parameter</span><span>val</span> <span>request</span> <span>=</span> <span>ListTodosRequest</span><span>.</span><span>newBuilder</span><span>()</span><span>.</span><span>setShowCompleted</span><span>(</span><span>showCompleted</span><span>)</span><span>.</span><span>build</span><span>()</span><span>// Retrieve list from gRPC server and return as MutableList</span><span>return</span> <span>todoStub</span><span>.</span><span>listTodos</span><span>(</span><span>request</span><span>).</span><span>todosList</span><span>}</span><span>// gRPC call to update an existing Todo on the server</span><span>fun</span> <span>updateTodo</span><span>(</span><span>id</span><span>:</span> <span>String</span><span>,</span> <span>title</span><span>:</span> <span>String</span><span>,</span> <span>description</span><span>:</span> <span>String</span><span>,</span> <span>completed</span><span>:</span> <span>Boolean</span><span>,</span> <span>dueDate</span><span>:</span> <span>Instant</span><span>?):</span> <span>TodoItem</span><span>?</span> <span>{</span><span>// Build gRPC request with updated Todo data</span><span>val</span> <span>request</span> <span>=</span> <span>TodoRequest</span><span>.</span><span>newBuilder</span><span>()</span><span>.</span><span>setId</span><span>(</span><span>id</span><span>)</span><span>.</span><span>setTitle</span><span>(</span><span>title</span><span>)</span><span>.</span><span>setDescription</span><span>(</span><span>description</span><span>)</span><span>.</span><span>setCompleted</span><span>(</span><span>completed</span><span>)</span><span>.</span><span>apply</span> <span>{</span><span>dueDate</span><span>?.</span><span>let</span> <span>{</span><span>setDueDate</span><span>(</span><span>Timestamp</span><span>.</span><span>newBuilder</span><span>()</span><span>.</span><span>setSeconds</span><span>(</span><span>it</span><span>.</span><span>epochSecond</span><span>)</span><span>.</span><span>setNanos</span><span>(</span><span>it</span><span>.</span><span>nano</span><span>)</span><span>.</span><span>build</span><span>())</span><span>}</span><span>}</span><span>.</span><span>build</span><span>()</span><span>// Update Todo via gRPC server and return result</span><span>return</span> <span>todoStub</span><span>.</span><span>updateTodo</span><span>(</span><span>request</span><span>).</span><span>todo</span><span>}</span><span>// gRPC call to delete a Todo by ID on the server</span><span>fun</span> <span>deleteTodo</span><span>(</span><span>id</span><span>:</span> <span>String</span><span>):</span> <span>Empty</span><span>?</span> <span>{</span><span>// Construct gRPC request with ID to delete</span><span>val</span> <span>request</span> <span>=</span> <span>TodoIdRequest</span><span>.</span><span>newBuilder</span><span>().</span><span>setId</span><span>(</span><span>id</span><span>).</span><span>build</span><span>()</span><span>// Delete Todo on gRPC server and return empty response</span><span>return</span> <span>todoStub</span><span>.</span><span>deleteTodo</span><span>(</span><span>request</span><span>)</span><span>}</span><span>}</span><span>package</span> <span>com.mahmud.grpcclient.service</span> <span>import</span> <span>com.google.protobuf.Timestamp</span> <span>import</span> <span>com.mahmud.grpcclient.*</span> <span>import</span> <span>org.springframework.stereotype.Service</span> <span>import</span> <span>java.time.Instant</span> <span>// Spring service managing gRPC client calls to the server</span> <span>@Service</span> <span>class</span> <span>TodoClientService</span><span>(</span><span>private</span> <span>val</span> <span>todoStub</span><span>:</span> <span>TodoServiceGrpc</span><span>.</span><span>TodoServiceBlockingStub</span><span>)</span> <span>{</span> <span>// gRPC call to create a new Todo on the server</span> <span>fun</span> <span>createTodo</span><span>(</span><span>title</span><span>:</span> <span>String</span><span>,</span> <span>description</span><span>:</span> <span>String</span><span>,</span> <span>completed</span><span>:</span> <span>Boolean</span><span>,</span> <span>dueDate</span><span>:</span> <span>Instant</span><span>?):</span> <span>TodoItem</span><span>?</span> <span>{</span> <span>// Build gRPC request from method parameters</span> <span>val</span> <span>request</span> <span>=</span> <span>TodoRequest</span><span>.</span><span>newBuilder</span><span>()</span> <span>.</span><span>setTitle</span><span>(</span><span>title</span><span>)</span> <span>.</span><span>setDescription</span><span>(</span><span>description</span><span>)</span> <span>.</span><span>setCompleted</span><span>(</span><span>completed</span><span>)</span> <span>.</span><span>apply</span> <span>{</span> <span>dueDate</span><span>?.</span><span>let</span> <span>{</span> <span>setDueDate</span><span>(</span><span>Timestamp</span><span>.</span><span>newBuilder</span><span>()</span> <span>.</span><span>setSeconds</span><span>(</span><span>it</span><span>.</span><span>epochSecond</span><span>)</span> <span>.</span><span>setNanos</span><span>(</span><span>it</span><span>.</span><span>nano</span><span>)</span> <span>.</span><span>build</span><span>())</span> <span>}</span> <span>}</span> <span>.</span><span>build</span><span>()</span> <span>// Invoke gRPC server method and return response</span> <span>return</span> <span>todoStub</span><span>.</span><span>createTodo</span><span>(</span><span>request</span><span>).</span><span>todo</span><span>;</span> <span>}</span> <span>// gRPC call to retrieve a Todo by its ID from the server</span> <span>fun</span> <span>getTodo</span><span>(</span><span>id</span><span>:</span> <span>String</span><span>):</span> <span>TodoItem</span> <span>{</span> <span>// Construct gRPC request with Todo ID</span> <span>val</span> <span>request</span> <span>=</span> <span>TodoIdRequest</span><span>.</span><span>newBuilder</span><span>().</span><span>setId</span><span>(</span><span>id</span><span>).</span><span>build</span><span>()</span> <span>// Fetch Todo from gRPC server and return it</span> <span>return</span> <span>todoStub</span><span>.</span><span>getTodo</span><span>(</span><span>request</span><span>).</span><span>todo</span> <span>}</span> <span>// gRPC call to list Todos, optionally showing completed ones</span> <span>fun</span> <span>listTodos</span><span>(</span><span>showCompleted</span><span>:</span> <span>Boolean</span><span>):</span> <span>MutableList</span><span><</span><span>TodoItem</span><span>>?</span> <span>{</span> <span>// Create gRPC request with filter parameter</span> <span>val</span> <span>request</span> <span>=</span> <span>ListTodosRequest</span><span>.</span><span>newBuilder</span><span>()</span> <span>.</span><span>setShowCompleted</span><span>(</span><span>showCompleted</span><span>)</span> <span>.</span><span>build</span><span>()</span> <span>// Retrieve list from gRPC server and return as MutableList</span> <span>return</span> <span>todoStub</span><span>.</span><span>listTodos</span><span>(</span><span>request</span><span>).</span><span>todosList</span> <span>}</span> <span>// gRPC call to update an existing Todo on the server</span> <span>fun</span> <span>updateTodo</span><span>(</span><span>id</span><span>:</span> <span>String</span><span>,</span> <span>title</span><span>:</span> <span>String</span><span>,</span> <span>description</span><span>:</span> <span>String</span><span>,</span> <span>completed</span><span>:</span> <span>Boolean</span><span>,</span> <span>dueDate</span><span>:</span> <span>Instant</span><span>?):</span> <span>TodoItem</span><span>?</span> <span>{</span> <span>// Build gRPC request with updated Todo data</span> <span>val</span> <span>request</span> <span>=</span> <span>TodoRequest</span><span>.</span><span>newBuilder</span><span>()</span> <span>.</span><span>setId</span><span>(</span><span>id</span><span>)</span> <span>.</span><span>setTitle</span><span>(</span><span>title</span><span>)</span> <span>.</span><span>setDescription</span><span>(</span><span>description</span><span>)</span> <span>.</span><span>setCompleted</span><span>(</span><span>completed</span><span>)</span> <span>.</span><span>apply</span> <span>{</span> <span>dueDate</span><span>?.</span><span>let</span> <span>{</span> <span>setDueDate</span><span>(</span><span>Timestamp</span><span>.</span><span>newBuilder</span><span>()</span> <span>.</span><span>setSeconds</span><span>(</span><span>it</span><span>.</span><span>epochSecond</span><span>)</span> <span>.</span><span>setNanos</span><span>(</span><span>it</span><span>.</span><span>nano</span><span>)</span> <span>.</span><span>build</span><span>())</span> <span>}</span> <span>}</span> <span>.</span><span>build</span><span>()</span> <span>// Update Todo via gRPC server and return result</span> <span>return</span> <span>todoStub</span><span>.</span><span>updateTodo</span><span>(</span><span>request</span><span>).</span><span>todo</span> <span>}</span> <span>// gRPC call to delete a Todo by ID on the server</span> <span>fun</span> <span>deleteTodo</span><span>(</span><span>id</span><span>:</span> <span>String</span><span>):</span> <span>Empty</span><span>?</span> <span>{</span> <span>// Construct gRPC request with ID to delete</span> <span>val</span> <span>request</span> <span>=</span> <span>TodoIdRequest</span><span>.</span><span>newBuilder</span><span>().</span><span>setId</span><span>(</span><span>id</span><span>).</span><span>build</span><span>()</span> <span>// Delete Todo on gRPC server and return empty response</span> <span>return</span> <span>todoStub</span><span>.</span><span>deleteTodo</span><span>(</span><span>request</span><span>)</span> <span>}</span> <span>}</span>package com.mahmud.grpcclient.service import com.google.protobuf.Timestamp import com.mahmud.grpcclient.* import org.springframework.stereotype.Service import java.time.Instant // Spring service managing gRPC client calls to the server @Service class TodoClientService(private val todoStub: TodoServiceGrpc.TodoServiceBlockingStub) { // gRPC call to create a new Todo on the server fun createTodo(title: String, description: String, completed: Boolean, dueDate: Instant?): TodoItem? { // Build gRPC request from method parameters val request = TodoRequest.newBuilder() .setTitle(title) .setDescription(description) .setCompleted(completed) .apply { dueDate?.let { setDueDate(Timestamp.newBuilder() .setSeconds(it.epochSecond) .setNanos(it.nano) .build()) } } .build() // Invoke gRPC server method and return response return todoStub.createTodo(request).todo; } // gRPC call to retrieve a Todo by its ID from the server fun getTodo(id: String): TodoItem { // Construct gRPC request with Todo ID val request = TodoIdRequest.newBuilder().setId(id).build() // Fetch Todo from gRPC server and return it return todoStub.getTodo(request).todo } // gRPC call to list Todos, optionally showing completed ones fun listTodos(showCompleted: Boolean): MutableList<TodoItem>? { // Create gRPC request with filter parameter val request = ListTodosRequest.newBuilder() .setShowCompleted(showCompleted) .build() // Retrieve list from gRPC server and return as MutableList return todoStub.listTodos(request).todosList } // gRPC call to update an existing Todo on the server fun updateTodo(id: String, title: String, description: String, completed: Boolean, dueDate: Instant?): TodoItem? { // Build gRPC request with updated Todo data val request = TodoRequest.newBuilder() .setId(id) .setTitle(title) .setDescription(description) .setCompleted(completed) .apply { dueDate?.let { setDueDate(Timestamp.newBuilder() .setSeconds(it.epochSecond) .setNanos(it.nano) .build()) } } .build() // Update Todo via gRPC server and return result return todoStub.updateTodo(request).todo } // gRPC call to delete a Todo by ID on the server fun deleteTodo(id: String): Empty? { // Construct gRPC request with ID to delete val request = TodoIdRequest.newBuilder().setId(id).build() // Delete Todo on gRPC server and return empty response return todoStub.deleteTodo(request) } }
Enter fullscreen mode Exit fullscreen mode
Breakdown:
-
@Service
: Marks as a Spring service. -
todoStub
: Injected stub for calls.
DTO
Let’s define the REST response DTO for JSON serialization.
TodoItemDto.kt
<span>package</span> <span>com.mahmud.grpcclient.dto</span><span>import</span> <span>com.mahmud.grpcclient.TodoItem</span><span>data class</span> <span>TodoItemDto</span><span>(</span><span>val</span> <span>id</span><span>:</span> <span>String</span><span>,</span><span>val</span> <span>title</span><span>:</span> <span>String</span><span>,</span><span>val</span> <span>description</span><span>:</span> <span>String</span><span>,</span><span>val</span> <span>completed</span><span>:</span> <span>Boolean</span><span>,</span><span>val</span> <span>createdAt</span><span>:</span> <span>Long</span><span>,</span> <span>// Epoch seconds</span><span>val</span> <span>updatedAt</span><span>:</span> <span>Long</span><span>,</span> <span>// Epoch seconds</span><span>val</span> <span>dueDate</span><span>:</span> <span>Long</span><span>?</span> <span>// dueDate nullable</span><span>)</span><span>fun</span> <span>mapToTodoItemDto</span><span>(</span><span>todoItem</span><span>:</span> <span>TodoItem</span><span>):</span> <span>TodoItemDto</span> <span>{</span><span>return</span> <span>TodoItemDto</span><span>(</span><span>id</span> <span>=</span> <span>todoItem</span><span>.</span><span>id</span><span>,</span><span>title</span> <span>=</span> <span>todoItem</span><span>.</span><span>title</span><span>,</span><span>description</span> <span>=</span> <span>todoItem</span><span>.</span><span>description</span><span>,</span><span>completed</span> <span>=</span> <span>todoItem</span><span>.</span><span>completed</span><span>,</span><span>createdAt</span> <span>=</span> <span>todoItem</span><span>.</span><span>createdAt</span><span>.</span><span>seconds</span><span>,</span><span>updatedAt</span> <span>=</span> <span>todoItem</span><span>.</span><span>updatedAt</span><span>.</span><span>seconds</span><span>,</span><span>dueDate</span> <span>=</span> <span>todoItem</span><span>.</span><span>dueDate</span><span>?.</span><span>seconds</span> <span>// handle the null case</span><span>)</span><span>}</span><span>package</span> <span>com.mahmud.grpcclient.dto</span> <span>import</span> <span>com.mahmud.grpcclient.TodoItem</span> <span>data class</span> <span>TodoItemDto</span><span>(</span> <span>val</span> <span>id</span><span>:</span> <span>String</span><span>,</span> <span>val</span> <span>title</span><span>:</span> <span>String</span><span>,</span> <span>val</span> <span>description</span><span>:</span> <span>String</span><span>,</span> <span>val</span> <span>completed</span><span>:</span> <span>Boolean</span><span>,</span> <span>val</span> <span>createdAt</span><span>:</span> <span>Long</span><span>,</span> <span>// Epoch seconds</span> <span>val</span> <span>updatedAt</span><span>:</span> <span>Long</span><span>,</span> <span>// Epoch seconds</span> <span>val</span> <span>dueDate</span><span>:</span> <span>Long</span><span>?</span> <span>// dueDate nullable</span> <span>)</span> <span>fun</span> <span>mapToTodoItemDto</span><span>(</span><span>todoItem</span><span>:</span> <span>TodoItem</span><span>):</span> <span>TodoItemDto</span> <span>{</span> <span>return</span> <span>TodoItemDto</span><span>(</span> <span>id</span> <span>=</span> <span>todoItem</span><span>.</span><span>id</span><span>,</span> <span>title</span> <span>=</span> <span>todoItem</span><span>.</span><span>title</span><span>,</span> <span>description</span> <span>=</span> <span>todoItem</span><span>.</span><span>description</span><span>,</span> <span>completed</span> <span>=</span> <span>todoItem</span><span>.</span><span>completed</span><span>,</span> <span>createdAt</span> <span>=</span> <span>todoItem</span><span>.</span><span>createdAt</span><span>.</span><span>seconds</span><span>,</span> <span>updatedAt</span> <span>=</span> <span>todoItem</span><span>.</span><span>updatedAt</span><span>.</span><span>seconds</span><span>,</span> <span>dueDate</span> <span>=</span> <span>todoItem</span><span>.</span><span>dueDate</span><span>?.</span><span>seconds</span> <span>// handle the null case</span> <span>)</span> <span>}</span>package com.mahmud.grpcclient.dto import com.mahmud.grpcclient.TodoItem data class TodoItemDto( val id: String, val title: String, val description: String, val completed: Boolean, val createdAt: Long, // Epoch seconds val updatedAt: Long, // Epoch seconds val dueDate: Long? // dueDate nullable ) fun mapToTodoItemDto(todoItem: TodoItem): TodoItemDto { return TodoItemDto( id = todoItem.id, title = todoItem.title, description = todoItem.description, completed = todoItem.completed, createdAt = todoItem.createdAt.seconds, updatedAt = todoItem.updatedAt.seconds, dueDate = todoItem.dueDate?.seconds // handle the null case ) }
Enter fullscreen mode Exit fullscreen mode
Define the DTO for Todo Request:
TodoRequestDto.kt
<span>package</span> <span>com.mahmud.grpcclient.dto</span><span>data class</span> <span>TodoRequestDto</span><span>(</span><span>val</span> <span>title</span><span>:</span> <span>String</span><span>,</span><span>val</span> <span>description</span><span>:</span> <span>String</span><span>,</span><span>val</span> <span>completed</span><span>:</span> <span>Boolean</span> <span>=</span> <span>false</span><span>,</span><span>val</span> <span>dueDate</span><span>:</span> <span>Long</span><span>?</span> <span>=</span> <span>null</span> <span>// Epoch seconds </span><span>)</span><span>package</span> <span>com.mahmud.grpcclient.dto</span> <span>data class</span> <span>TodoRequestDto</span><span>(</span> <span>val</span> <span>title</span><span>:</span> <span>String</span><span>,</span> <span>val</span> <span>description</span><span>:</span> <span>String</span><span>,</span> <span>val</span> <span>completed</span><span>:</span> <span>Boolean</span> <span>=</span> <span>false</span><span>,</span> <span>val</span> <span>dueDate</span><span>:</span> <span>Long</span><span>?</span> <span>=</span> <span>null</span> <span>// Epoch seconds </span> <span>)</span>package com.mahmud.grpcclient.dto data class TodoRequestDto( val title: String, val description: String, val completed: Boolean = false, val dueDate: Long? = null // Epoch seconds )
Enter fullscreen mode Exit fullscreen mode
Expose REST endpoints
TodoController.kt
REST controller bridging HTTP requests to gRPC calls with detailed comments.
<span>package</span> <span>com.mahmud.grpcclient.controller</span><span>import</span> <span>com.mahmud.grpcclient.Empty</span><span>import</span> <span>com.mahmud.grpcclient.service.TodoClientService</span><span>import</span> <span>com.mahmud.grpcclient.TodoItem</span><span>import</span> <span>com.mahmud.grpcclient.dto.TodoItemDto</span><span>import</span> <span>com.mahmud.grpcclient.dto.TodoRequestDto</span><span>import</span> <span>com.mahmud.grpcclient.dto.mapToTodoItemDto</span><span>import</span> <span>org.springframework.web.bind.annotation.*</span><span>import</span> <span>reactor.core.publisher.Flux</span><span>import</span> <span>reactor.core.publisher.Mono</span><span>import</span> <span>java.time.Instant</span><span>// REST controller exposing Todo endpoints over HTTP</span><span>@RestController</span><span>@RequestMapping</span><span>(</span><span>"/api/todos"</span><span>)</span><span>class</span> <span>TodoController</span><span>(</span><span>private</span> <span>val</span> <span>todoService</span><span>:</span> <span>TodoClientService</span><span>)</span> <span>{</span><span>// POST endpoint to create a new Todo via gRPC</span><span>@PostMapping</span><span>fun</span> <span>createTodo</span><span>(</span><span>@RequestBody</span> <span>request</span><span>:</span> <span>TodoRequestDto</span><span>):</span> <span>Mono</span><span><</span><span>TodoItemDto</span><span>></span> <span>{</span><span>// Delegate to gRPC service to create Todo</span><span>val</span> <span>todo</span> <span>=</span> <span>todoService</span><span>.</span><span>createTodo</span><span>(</span><span>request</span><span>.</span><span>title</span><span>,</span><span>request</span><span>.</span><span>description</span><span>,</span><span>request</span><span>.</span><span>completed</span><span>,</span><span>request</span><span>.</span><span>dueDate</span><span>?.</span><span>let</span> <span>{</span> <span>Instant</span><span>.</span><span>ofEpochSecond</span><span>(</span><span>it</span><span>)</span> <span>}</span><span>)</span><span>// Wrap gRPC response in reactive Mono for REST</span><span>return</span> <span>Mono</span><span>.</span><span>justOrEmpty</span><span>(</span><span>todo</span><span>?.</span><span>let</span> <span>{</span> <span>mapToTodoItemDto</span><span>(</span><span>it</span><span>)</span> <span>})</span><span>}</span><span>// GET endpoint to retrieve a Todo by ID via gRPC</span><span>@GetMapping</span><span>(</span><span>"/{id}"</span><span>)</span><span>fun</span> <span>getTodo</span><span>(</span><span>@PathVariable</span> <span>id</span><span>:</span> <span>String</span><span>):</span> <span>Mono</span><span><</span><span>TodoItemDto</span><span>></span> <span>{</span><span>// Fetch Todo from gRPC service and map to DTO</span><span>return</span> <span>Mono</span><span>.</span><span>justOrEmpty</span><span>(</span><span>mapToTodoItemDto</span><span>(</span><span>todoService</span><span>.</span><span>getTodo</span><span>(</span><span>id</span><span>)))</span><span>}</span><span>// GET endpoint to list Todos, with optional completion filter</span><span>@GetMapping</span><span>fun</span> <span>listTodos</span><span>(</span><span>@RequestParam</span><span>(</span><span>defaultValue</span> <span>=</span> <span>"true"</span><span>)</span> <span>showCompleted</span><span>:</span> <span>Boolean</span><span>):</span> <span>Flux</span><span><</span><span>TodoItemDto</span><span>></span> <span>{</span><span>// Retrieve list from gRPC service</span><span>val</span> <span>todoList</span> <span>=</span> <span>todoService</span><span>.</span><span>listTodos</span><span>(</span><span>showCompleted</span><span>)</span><span>// Convert gRPC list to reactive Flux of DTOs</span><span>return</span> <span>todoList</span><span>?.</span><span>let</span> <span>{</span> <span>Flux</span><span>.</span><span>fromIterable</span><span>(</span><span>it</span><span>).</span><span>map</span> <span>{</span> <span>todoItem</span> <span>-></span> <span>mapToTodoItemDto</span><span>(</span><span>todoItem</span><span>)</span> <span>}</span> <span>}</span> <span>?:</span> <span>Flux</span><span>.</span><span>empty</span><span>()</span><span>}</span><span>// PUT endpoint to update a Todo via gRPC</span><span>@PutMapping</span><span>(</span><span>"/{id}"</span><span>)</span><span>fun</span> <span>updateTodo</span><span>(</span><span>@PathVariable</span> <span>id</span><span>:</span> <span>String</span><span>,</span><span>@RequestBody</span> <span>request</span><span>:</span> <span>TodoRequestDto</span><span>):</span> <span>Mono</span><span><</span><span>TodoItemDto</span><span>></span> <span>{</span><span>// Update Todo using gRPC service</span><span>val</span> <span>todo</span> <span>=</span> <span>todoService</span><span>.</span><span>updateTodo</span><span>(</span><span>id</span><span>,</span><span>request</span><span>.</span><span>title</span><span>,</span><span>request</span><span>.</span><span>description</span><span>,</span><span>request</span><span>.</span><span>completed</span><span>,</span><span>request</span><span>.</span><span>dueDate</span><span>?.</span><span>let</span> <span>{</span> <span>Instant</span><span>.</span><span>ofEpochSecond</span><span>(</span><span>it</span><span>)</span> <span>}</span><span>)</span><span>// Return updated Todo as reactive Mono</span><span>return</span> <span>Mono</span><span>.</span><span>justOrEmpty</span><span>(</span><span>todo</span><span>?.</span><span>let</span> <span>{</span> <span>mapToTodoItemDto</span><span>(</span><span>it</span><span>)</span> <span>})</span><span>}</span><span>// DELETE endpoint to remove a Todo via gRPC</span><span>@DeleteMapping</span><span>(</span><span>"/{id}"</span><span>)</span><span>fun</span> <span>deleteTodo</span><span>(</span><span>@PathVariable</span> <span>id</span><span>:</span> <span>String</span><span>):</span> <span>Empty</span><span>?</span> <span>{</span><span>// Call gRPC service to delete Todo and return empty response</span><span>return</span> <span>todoService</span><span>.</span><span>deleteTodo</span><span>(</span><span>id</span><span>)</span><span>}</span><span>}</span><span>package</span> <span>com.mahmud.grpcclient.controller</span> <span>import</span> <span>com.mahmud.grpcclient.Empty</span> <span>import</span> <span>com.mahmud.grpcclient.service.TodoClientService</span> <span>import</span> <span>com.mahmud.grpcclient.TodoItem</span> <span>import</span> <span>com.mahmud.grpcclient.dto.TodoItemDto</span> <span>import</span> <span>com.mahmud.grpcclient.dto.TodoRequestDto</span> <span>import</span> <span>com.mahmud.grpcclient.dto.mapToTodoItemDto</span> <span>import</span> <span>org.springframework.web.bind.annotation.*</span> <span>import</span> <span>reactor.core.publisher.Flux</span> <span>import</span> <span>reactor.core.publisher.Mono</span> <span>import</span> <span>java.time.Instant</span> <span>// REST controller exposing Todo endpoints over HTTP</span> <span>@RestController</span> <span>@RequestMapping</span><span>(</span><span>"/api/todos"</span><span>)</span> <span>class</span> <span>TodoController</span><span>(</span><span>private</span> <span>val</span> <span>todoService</span><span>:</span> <span>TodoClientService</span><span>)</span> <span>{</span> <span>// POST endpoint to create a new Todo via gRPC</span> <span>@PostMapping</span> <span>fun</span> <span>createTodo</span><span>(</span> <span>@RequestBody</span> <span>request</span><span>:</span> <span>TodoRequestDto</span> <span>):</span> <span>Mono</span><span><</span><span>TodoItemDto</span><span>></span> <span>{</span> <span>// Delegate to gRPC service to create Todo</span> <span>val</span> <span>todo</span> <span>=</span> <span>todoService</span><span>.</span><span>createTodo</span><span>(</span> <span>request</span><span>.</span><span>title</span><span>,</span> <span>request</span><span>.</span><span>description</span><span>,</span> <span>request</span><span>.</span><span>completed</span><span>,</span> <span>request</span><span>.</span><span>dueDate</span><span>?.</span><span>let</span> <span>{</span> <span>Instant</span><span>.</span><span>ofEpochSecond</span><span>(</span><span>it</span><span>)</span> <span>}</span> <span>)</span> <span>// Wrap gRPC response in reactive Mono for REST</span> <span>return</span> <span>Mono</span><span>.</span><span>justOrEmpty</span><span>(</span><span>todo</span><span>?.</span><span>let</span> <span>{</span> <span>mapToTodoItemDto</span><span>(</span><span>it</span><span>)</span> <span>})</span> <span>}</span> <span>// GET endpoint to retrieve a Todo by ID via gRPC</span> <span>@GetMapping</span><span>(</span><span>"/{id}"</span><span>)</span> <span>fun</span> <span>getTodo</span><span>(</span><span>@PathVariable</span> <span>id</span><span>:</span> <span>String</span><span>):</span> <span>Mono</span><span><</span><span>TodoItemDto</span><span>></span> <span>{</span> <span>// Fetch Todo from gRPC service and map to DTO</span> <span>return</span> <span>Mono</span><span>.</span><span>justOrEmpty</span><span>(</span><span>mapToTodoItemDto</span><span>(</span><span>todoService</span><span>.</span><span>getTodo</span><span>(</span><span>id</span><span>)))</span> <span>}</span> <span>// GET endpoint to list Todos, with optional completion filter</span> <span>@GetMapping</span> <span>fun</span> <span>listTodos</span><span>(</span><span>@RequestParam</span><span>(</span><span>defaultValue</span> <span>=</span> <span>"true"</span><span>)</span> <span>showCompleted</span><span>:</span> <span>Boolean</span><span>):</span> <span>Flux</span><span><</span><span>TodoItemDto</span><span>></span> <span>{</span> <span>// Retrieve list from gRPC service</span> <span>val</span> <span>todoList</span> <span>=</span> <span>todoService</span><span>.</span><span>listTodos</span><span>(</span><span>showCompleted</span><span>)</span> <span>// Convert gRPC list to reactive Flux of DTOs</span> <span>return</span> <span>todoList</span><span>?.</span><span>let</span> <span>{</span> <span>Flux</span><span>.</span><span>fromIterable</span><span>(</span><span>it</span><span>).</span><span>map</span> <span>{</span> <span>todoItem</span> <span>-></span> <span>mapToTodoItemDto</span><span>(</span><span>todoItem</span><span>)</span> <span>}</span> <span>}</span> <span>?:</span> <span>Flux</span><span>.</span><span>empty</span><span>()</span> <span>}</span> <span>// PUT endpoint to update a Todo via gRPC</span> <span>@PutMapping</span><span>(</span><span>"/{id}"</span><span>)</span> <span>fun</span> <span>updateTodo</span><span>(</span> <span>@PathVariable</span> <span>id</span><span>:</span> <span>String</span><span>,</span> <span>@RequestBody</span> <span>request</span><span>:</span> <span>TodoRequestDto</span> <span>):</span> <span>Mono</span><span><</span><span>TodoItemDto</span><span>></span> <span>{</span> <span>// Update Todo using gRPC service</span> <span>val</span> <span>todo</span> <span>=</span> <span>todoService</span><span>.</span><span>updateTodo</span><span>(</span> <span>id</span><span>,</span> <span>request</span><span>.</span><span>title</span><span>,</span> <span>request</span><span>.</span><span>description</span><span>,</span> <span>request</span><span>.</span><span>completed</span><span>,</span> <span>request</span><span>.</span><span>dueDate</span><span>?.</span><span>let</span> <span>{</span> <span>Instant</span><span>.</span><span>ofEpochSecond</span><span>(</span><span>it</span><span>)</span> <span>}</span> <span>)</span> <span>// Return updated Todo as reactive Mono</span> <span>return</span> <span>Mono</span><span>.</span><span>justOrEmpty</span><span>(</span><span>todo</span><span>?.</span><span>let</span> <span>{</span> <span>mapToTodoItemDto</span><span>(</span><span>it</span><span>)</span> <span>})</span> <span>}</span> <span>// DELETE endpoint to remove a Todo via gRPC</span> <span>@DeleteMapping</span><span>(</span><span>"/{id}"</span><span>)</span> <span>fun</span> <span>deleteTodo</span><span>(</span><span>@PathVariable</span> <span>id</span><span>:</span> <span>String</span><span>):</span> <span>Empty</span><span>?</span> <span>{</span> <span>// Call gRPC service to delete Todo and return empty response</span> <span>return</span> <span>todoService</span><span>.</span><span>deleteTodo</span><span>(</span><span>id</span><span>)</span> <span>}</span> <span>}</span>package com.mahmud.grpcclient.controller import com.mahmud.grpcclient.Empty import com.mahmud.grpcclient.service.TodoClientService import com.mahmud.grpcclient.TodoItem import com.mahmud.grpcclient.dto.TodoItemDto import com.mahmud.grpcclient.dto.TodoRequestDto import com.mahmud.grpcclient.dto.mapToTodoItemDto import org.springframework.web.bind.annotation.* import reactor.core.publisher.Flux import reactor.core.publisher.Mono import java.time.Instant // REST controller exposing Todo endpoints over HTTP @RestController @RequestMapping("/api/todos") class TodoController(private val todoService: TodoClientService) { // POST endpoint to create a new Todo via gRPC @PostMapping fun createTodo( @RequestBody request: TodoRequestDto ): Mono<TodoItemDto> { // Delegate to gRPC service to create Todo val todo = todoService.createTodo( request.title, request.description, request.completed, request.dueDate?.let { Instant.ofEpochSecond(it) } ) // Wrap gRPC response in reactive Mono for REST return Mono.justOrEmpty(todo?.let { mapToTodoItemDto(it) }) } // GET endpoint to retrieve a Todo by ID via gRPC @GetMapping("/{id}") fun getTodo(@PathVariable id: String): Mono<TodoItemDto> { // Fetch Todo from gRPC service and map to DTO return Mono.justOrEmpty(mapToTodoItemDto(todoService.getTodo(id))) } // GET endpoint to list Todos, with optional completion filter @GetMapping fun listTodos(@RequestParam(defaultValue = "true") showCompleted: Boolean): Flux<TodoItemDto> { // Retrieve list from gRPC service val todoList = todoService.listTodos(showCompleted) // Convert gRPC list to reactive Flux of DTOs return todoList?.let { Flux.fromIterable(it).map { todoItem -> mapToTodoItemDto(todoItem) } } ?: Flux.empty() } // PUT endpoint to update a Todo via gRPC @PutMapping("/{id}") fun updateTodo( @PathVariable id: String, @RequestBody request: TodoRequestDto ): Mono<TodoItemDto> { // Update Todo using gRPC service val todo = todoService.updateTodo( id, request.title, request.description, request.completed, request.dueDate?.let { Instant.ofEpochSecond(it) } ) // Return updated Todo as reactive Mono return Mono.justOrEmpty(todo?.let { mapToTodoItemDto(it) }) } // DELETE endpoint to remove a Todo via gRPC @DeleteMapping("/{id}") fun deleteTodo(@PathVariable id: String): Empty? { // Call gRPC service to delete Todo and return empty response return todoService.deleteTodo(id) } }
Enter fullscreen mode Exit fullscreen mode
Breakdown:
-
@RestController
: Exposes REST endpoints. -
Mono
/Flux
: Handles reactive output.
Step 5: Run the Client
./gradlew bootRun./gradlew bootRun./gradlew bootRun
Enter fullscreen mode Exit fullscreen mode
LEt’s test the gRPC Client using curl
The client exposes REST endpoints at http://localhost:8080/api/todos
, mapping to the gRPC server’s functionality.
1. Create Todo (POST)
Creates a new Todo via the REST API.
curl <span>-X</span> POST http://localhost:8080/api/todos <span>\</span><span>-H</span> <span>"Content-Type: application/json"</span> <span>\</span><span>-d</span> <span>'{ "title": "Buy groceries", "description": "Milk, bread, eggs", "completed": false, "dueDate": 1735689600 }'</span>curl <span>-X</span> POST http://localhost:8080/api/todos <span>\</span> <span>-H</span> <span>"Content-Type: application/json"</span> <span>\</span> <span>-d</span> <span>'{ "title": "Buy groceries", "description": "Milk, bread, eggs", "completed": false, "dueDate": 1735689600 }'</span>curl -X POST http://localhost:8080/api/todos \ -H "Content-Type: application/json" \ -d '{ "title": "Buy groceries", "description": "Milk, bread, eggs", "completed": false, "dueDate": 1735689600 }'
Enter fullscreen mode Exit fullscreen mode
- Payload:
TodoRequestDto
withdueDate
as epoch seconds (Dec 31, 2025, 00:00:00 UTC). - Response: JSON
TodoItemDto
with the created Todo.
2. Get Todo (GET)
Retrieves a Todo by ID (replace <todo-id>
with an actual ID).
curl <span>-X</span> GET http://localhost:8080/api/todos/<todo-id>curl <span>-X</span> GET http://localhost:8080/api/todos/<todo-id>curl -X GET http://localhost:8080/api/todos/<todo-id>
Enter fullscreen mode Exit fullscreen mode
- Path: Uses the Todo’s ID.
- Response: JSON
TodoItemDto
with the Todo’s details.
3. List Todos (GET)
Lists all Todos, with an optional showCompleted
filter.
curl <span>-X</span> GET <span>"http://localhost:8080/api/todos?showCompleted=true"</span>curl <span>-X</span> GET <span>"http://localhost:8080/api/todos?showCompleted=true"</span>curl -X GET "http://localhost:8080/api/todos?showCompleted=true"
Enter fullscreen mode Exit fullscreen mode
- Query param:
showCompleted=true
includes completed Todos; set tofalse
to exclude them. - Response: JSON array of
TodoItemDto
s.
4. Update Todo (PUT)
Updates an existing Todo (replace <todo-id>
with an actual ID).
curl <span>-X</span> PUT http://localhost:8080/api/todos/<todo-id> <span>\</span><span>-H</span> <span>"Content-Type: application/json"</span> <span>\</span><span>-d</span> <span>'{ "title": "Buy groceries updated", "description": "Milk, bread, eggs, cheese", "completed": true, "dueDate": 1735776000 }'</span>curl <span>-X</span> PUT http://localhost:8080/api/todos/<todo-id> <span>\</span> <span>-H</span> <span>"Content-Type: application/json"</span> <span>\</span> <span>-d</span> <span>'{ "title": "Buy groceries updated", "description": "Milk, bread, eggs, cheese", "completed": true, "dueDate": 1735776000 }'</span>curl -X PUT http://localhost:8080/api/todos/<todo-id> \ -H "Content-Type: application/json" \ -d '{ "title": "Buy groceries updated", "description": "Milk, bread, eggs, cheese", "completed": true, "dueDate": 1735776000 }'
Enter fullscreen mode Exit fullscreen mode
- Payload:
TodoRequestDto
with updated fields,dueDate
as epoch seconds (Jan 1, 2026, 00:00:00 UTC). - Response: JSON
TodoItemDto
with the updated Todo.
5. Delete Todo (DELETE)
Deletes a Todo by ID (replace <todo-id>
with an actual ID).
curl <span>-X</span> DELETE http://localhost:8080/api/todos/<todo-id>curl <span>-X</span> DELETE http://localhost:8080/api/todos/<todo-id>curl -X DELETE http://localhost:8080/api/todos/<todo-id>
Enter fullscreen mode Exit fullscreen mode
- Path: Uses the Todo’s ID.
- Response: Empty response (HTTP 200 OK).
Conclusion
This guide provides a complete implementation of a Todo application using gRPC with Spring Boot 3 and Spring gRPC. By building the server and client sequentially, we’ve demonstrated gRPC’s capabilities within a Spring ecosystem, leveraging Kotlin and MongoDB.
Key takeaways:
- gRPC’s performance and typing advantages make it ideal for service-to-service communication.
- Spring gRPC simplifies gRPC adoption in Spring Boot, despite its experimental status.
- Reactive programming enhances scalability.
For production, consider TLS, full reactive flows, and streaming features. As Spring gRPC matures, it will solidify its place in modern architectures.
References
- gRPC Official Documentation
- Spring gRPC GitHub
- Spring Boot 3 Documentation
- Protocol Buffers Documentation
- Kotlin Documentation
- MongoDB Reactive Streams
原文链接:Implementing gRPC with Kotlin & Spring Boot 3: A Comprehensive Guide
暂无评论内容