In the last post, we have achieved a good milestone of using a real datastore for storing and retrieving the records that the users are creating. We have already covered the services of users and todos for various CRUD operations.
This post will focus on polishing the application we have already built so that we can tighten the loose ends we left behind while covering the essentials.
This post will talk about some of the essential and some slightly advanced gRPC and web development concepts like the need for pagination, the importance of authentication and how interceptors make complex task easy. So let’s begin this journey.
Pagination
If you are completely new to web development then the concept of pagination simply means that we try to fetch records in batches or chunks. Suppose a user is using our applications for years and during his tenure he has more than thousands of todos.
In such scenarios it’s not a wise idea to send all the todos at once in the FindAll or GetAll unary RPCs. This is because the data volume will be too high which can cause load on the database, the frontend application (like web or mobile) which is fetching the data.
Google has a guideline for the pagination for RPCs, here’s the link API 158.
Since adding pagination during later stages of the application is backwards incompatible, we should ideally support pagination from the starting of the listing RPCs. This may sound like an anti-pattern in our series of blogs but since our application has not live users this is okay for us.
We will be using a cursor pagination where the cursor is actually the created_at field of the todo.
ProtoBuf Update
To allow pagination, we need to update both the Listing RPCs of the Todo and User proto file. For the file protos/todo.proto the changes will be:
func(ts *TodoService) ListTodos(_ context.Context, req *pb.ListTodosRequest) (*pb.ListTodosResponse, error) { // check if req has valid params size := req.GetPageSize() if size == 0 { size = 10 } if size > 1000 { size = 1000 } if size < 0 { returnnil, status.Error(codes.InvalidArgument, "page size must be positive") }
// token will be the created_at of the last todo in the previous page token := req.GetPageToken() last_created_at := time.Now() var err error if token != "" { last_created_at, err = time.Parse(time.RFC3339, token)
// get all from db todos, err := ts.repo.FindAll(last_created_at, size) if err != nil { log.Printf("error listing todos: %+v", err) returnnil, status.Error(codes.Internal, "something broke - find all todos") }
There are multiple changes which we have done in the service RPC for the ListTodos. let’s give a walkthrough of the changes:
Accepting the request for page_size and page_token.
Validating that the size is not invalid with proper gRPC errors
Passing the size and token to the repository method.
Handling the time format for the token.
Returning the next_page_token for the front-end to use for subsequent calls
Now from the grpcui, we can pass the page size and also the page token for our listing RPC calls.
Updated listing RCP with page size and page token
Repository Updates
We just saw that we have to pass the changes to the MySQL repository so that the database can return adequate response for the requested data. Here’s the repository changes we have to do:
Accepting the page size and the token in the arguments
Using the values with conditional checks for query construction
Make sure to properly order the LIMIT and ORDER BY and converting the size to Integer
Everything else remains the same. Now that we have the pagination in place we will move to the security aspect of the application and let’s explore authentication using JWT.
Authentication
Currently our app is open for all and for everything.
What I mean to say to is that suppose there are two users John and Johnny, thy will register themselves and start adding data to the application. Surprisingly they can not only see each other’s data they can also manipulate the data of one another. There’s no boundary no validation whether the data being manipulated is actually owned by the manipulator or not.
Thant’s when we need Authentication. It’s a boundary that can bring in certain restrictions of data usages and manipulations.
If you are already familiar with REST APIs you might have encountered JWT or JSON Web Token already.
JWT
A JWT is a compact token that represents a set of claims (data) which is stateless and is signed usually with a secret or private key. The format of a JWT string token usually has three parts and looks like: header.payload.signature
Using JWT
We will create a JWT token when the user will try to login after registration.
Notes
We are not using passwords or OTP for login, for now we will demonstrate login using email or username, if it is present in the database we will allow login and provide them the JWT token on successful login.
It’s a good exercise for you to add support for passwords for the users. But be careful to not store plain password in the database. Store the hashed password in the database.
Let’s add a file lib/jwt.go which will contain the code to create a token using uuid of the user and also to validate the token.
So now we have the JWT which we can use to check if the user who is calling the RPC is a valid register user of the system. How can we do that?
We need something like a middle man who can intercept the RPC call and validate that it is a valid call before it hit’s the business logic and the data layer. This middleman concept in the gRPC framework is called interceptor.
In Node.js and RubyOnRails, we call them middlewares and in a few frameworks we also call them filters.
Let’s write an interceptor function to check if the requested RPC call is having a JWT token in it and it’s a valid token which can yield us a valid uuid.
Request Metadata
We have already seen the Metadata section when invoking a RPC call via grpcui. Let’s explore more about it.
Metadata is a side channel that allows clients and servers to provide information to each other that is associated with an RPC. gRPC metadata is a key-value pair of data that is sent with initial or final gRPC requests or responses. It is used to provide additional information about the call, such as authentication credentials, tracing information, or custom headers.
We will add the JWT token received after the Login RPC call in the Request Metadata of the subsequent RPC calls.
The JWT token added in the Request Metadata in grpcui
Interceptor Function
Now we will write the interceptor function which will validate the JWT token and also it will extract and add the uuid of the user in the context.
func(ts *TodoService) CreateTodo(ctx context.Context, req *pb.CreateTodoRequest) (*pb.CreateTodoResponse, error) { // get the uuid of the user from context uuid, ok := ctx.Value(lib.UserContextUUIDKey).(string) if !ok || uuid == "" { returnnil, status.Error(codes.Unauthenticated, "unable to identify user") }
// create the todo todo := &pb.Todo{ Title: strings.TrimSpace(req.GetTitle()), Priority: req.GetPriority(), Done: false, CreatedAt: timestamppb.Now(), UserUuid: uuid, }
// save in DB todo, err := ts.repo.Save(todo) if err != nil { log.Printf("error creating todo: %+v", err) returnnil, status.Error(codes.Internal, fmt.Sprintf("failed to create Todo: %v", err)) }
// return the response return &pb.CreateTodoResponse{ Todo: todo, }, nil }
Once we do this we can see that the user_id is getting attached to the todo and is also saved in the database.
1 2 3 4 5 6 7 8 9
{ "title":"Implement JWT in gRPC", "description":"We will implement the interceptor and wire up the missing parts", "done":false, "priority":"PRIORITY_LOW", "created_at":"2023-02-25T13:51:06Z", "uuid":"b78423df-7395-4142-95b3-657aac419fb2", "user_uuid":"0901b0c0-7409-43a5-87b8-0aedd351b9d9" }
Caution: Handle Null UUID
Now when we have wired up the todos with the user_id we will face a small glitch while calling the GetTodo and ListTodos. The glitch will be caused by the null values in the user_id field of the existing todo records in the database. Let’s fix that in the repository layer:
Finally while updating or deleting the todo, we will validate if the caller of the RPC is actually the owner of the todo or not. Accordingly we will respond to the requests.
This is actually the concept of Authorization.
Authentication validates if the system can identify the caller but authorization validates whether the caller can perform certain actions on a resource.
Let’s make the required changes in the service layer.
func(ts *TodoService) UpdateTodo(ctx context.Context, req *pb.UpdateTodoRequest) (*pb.UpdateTodoResponse, error) { if req.GetUuid() == "" { returnnil, status.Error(codes.InvalidArgument, "ID should be positive") }
// get the uuid of the user from context uuid, ok := ctx.Value(lib.UserContextUUIDKey).(string) if !ok || uuid == "" { returnnil, status.Error(codes.Unauthenticated, "unable to identify user") }
// get todo by id val, err := ts.repo.FindByID(req.GetUuid()) if err != nil { log.Printf("error updating todo: %+v", err) returnnil, status.Error(codes.Internal, fmt.Sprintf("failed to get Todo: %v", err)) }
if val.GetUserUuid() != uuid { returnnil, status.Error(codes.PermissionDenied, "you don't have permission to update this todo") }
// update the values from req val.Title = req.GetTitle() val.Done = req.GetDone() val.Uuid = req.GetUuid()
func(ts *TodoService) DeleteTodo(ctx context.Context, req *pb.DeleteTodoRequest) (*emptypb.Empty, error) { if req.GetUuid() == "" { returnnil, status.Error(codes.InvalidArgument, "ID should be positive") }
// get the uuid of the user from context uuid, ok := ctx.Value(lib.UserContextUUIDKey).(string) if !ok || uuid == "" { returnnil, status.Error(codes.Unauthenticated, "unable to identify user") }
// get todo by id val, err := ts.repo.FindByID(req.GetUuid()) if err != nil { log.Printf("error updating todo: %+v", err) returnnil, status.Error(codes.Internal, fmt.Sprintf("failed to get Todo: %v", err)) } if val.GetUserUuid() != uuid { returnnil, status.Error(codes.PermissionDenied, "you don't have permission to update this todo") }
Now that we have JWT code implemented and we are providing the JWT token to the user, we have to fix a few more loose ends. Let me list down them or you might already know.
The user email and username should be unique.
Using migrations to track the database changes.
Using timeouts from the client and handling them in the server
Fixing MySQL Tables
In the users table we have to make the username and the email unique so that we do not end up having multiple rows for same user. We can use the alter command to make the field value unique.
Now we can see that there are series of changes for the database as well and we must keep a track of them so that we can replicate them to multiple environments. Not only that we can even revert them in case we feel that a particular database change was not fruitful.
We carry out such tracked database changes va migrations. We will look into that in future posts.
Conclusion
In this post, we took our gRPC application a step closer to production readiness by adding pagination, authentication, and authorization. We implemented cursor-based pagination to handle large datasets efficiently, secured our RPCs using JWT, and leveraged gRPC interceptors to keep authentication logic out of our core services.
By enforcing ownership checks at the service layer, we ensured that users can only access and manipulate their own data—clearly separating authentication from authorization.
These patterns are widely used in real-world gRPC systems and form a strong foundation for building secure, scalable backend services. In the upcoming posts, we will continue refining this application by focusing on operational and scalability aspects, including database migrations, request deadlines, and better error handling strategies.