Building a gRPC CRUD Application in Go: A Complete Guide
In this guide, I’ll walk you through implementing a gRPC-based banking application in Go, covering everything from protocol buffer definitions to database integration and testing.
Project Overview
This project demonstrates a full-featured CRUD application with the following capabilities:
- Customer account management (Create, Read, Update, Delete, List)
- Bank account operations
- Transaction processing and statement generation
- OAuth 2.0 JWT-based authentication (coming in future parts)
Getting Started
Project Initialization
First, create your project directory and initialize the Go module:
go mod init github.com/paudelanil/grpc-crud
This command creates a go.mod file that tracks your project’s dependencies.
Understanding Protocol Buffers and gRPC
What is a Proto File?
A proto file (.proto) is a language-agnostic interface definition file used in gRPC. It serves as a contract between client and server, defining data structures and service methods using Protocol Buffers—Google’s efficient serialization mechanism.
Key Components:
- Messages: Define the structure of data exchanged between client and server
- Services: Declare RPC methods available for remote invocation
- Field Types: Specify data types (string, int32, bool, etc.)
- Field Numbers: Unique identifiers for serialization (these numbers should never change once in production)
The Protocol Buffer compiler (protoc) transforms .proto files into language-specific code. For Go, this generates .pb.go files containing structs and gRPC service interfaces.
Learn More:
Defining Our Service
Project Structure
root/
├── proto/
│ └── user_account.proto
├── pb/
├── models/
├── service/
└── cmd/
└── server/
Creating the Proto File
Create proto/user_account.proto:
syntax = "proto3";
package grpc_crud;
option go_package = "github.com/paudelanil/grpc_crud/pb";
// Service for managing customer profiles and bank accounts
service AccountService {
// Customer Operations
rpc CreateUser(CreateCustomerRequest) returns (CreateCustomerResponse) {}
rpc GetUser(GetCustomerRequest) returns (GetCustomerResponse) {}
rpc UpdateUser(UpdateCustomerRequest) returns (UpdateCustomerResponse) {}
rpc DeleteUser(DeleteCustomerRequest) returns (DeleteCustomerResponse) {}
rpc ListUsers(ListCustomerRequest) returns (ListCustomerResponse) {}
// Account Operations
rpc CreateAccount(CreateAccountRequest) returns (CreateAccountResponse) {}
rpc GetAccount(GetAccountRequest) returns (GetAccountResponse) {}
rpc UpdateAccount(UpdateAccountRequest) returns (UpdateAccountResponse) {}
rpc DeleteAccount(DeleteAccountRequest) returns (DeleteAccountResponse) {}
rpc ListAccounts(ListAccountRequest) returns (ListAccountResponse) {}
}
// ============================================
// Customer Message Definitions
// ============================================
message CreateCustomerRequest {
string first_name = 1;
string last_name = 2;
string email = 3;
string phone_number = 4;
string address = 5;
}
message CreateCustomerResponse {
string customer_id = 1;
string message = 2;
}
message GetCustomerRequest {
string customer_id = 1;
}
message GetCustomerResponse {
string customer_id = 1;
string first_name = 2;
string last_name = 3;
string email = 4;
string phone_number = 5;
string address = 6;
string created_at = 7;
string updated_at = 8;
}
message UpdateCustomerRequest {
string customer_id = 1;
string first_name = 2;
string last_name = 3;
string email = 4;
string phone_number = 5;
string address = 6;
}
message UpdateCustomerResponse {
string message = 1;
GetCustomerResponse customer = 2;
}
message DeleteCustomerRequest {
string customer_id = 1;
}
message DeleteCustomerResponse {
string message = 1;
}
message ListCustomerRequest {
int32 page_number = 1;
int32 page_size = 2;
}
message ListCustomerResponse {
repeated GetCustomerResponse customers = 1;
int32 total_count = 2;
}
// ============================================
// Bank Account Message Definitions
// ============================================
message CreateAccountRequest {
string customer_id = 1;
string account_type = 2; // e.g., "savings", "checking"
string currency = 3; // e.g., "USD", "EUR", "NPR"
}
message CreateAccountResponse {
string account_id = 1;
string account_number = 2;
string message = 3;
}
message GetAccountRequest {
string account_id = 1;
}
message GetAccountResponse {
string account_id = 1;
string account_number = 2;
string customer_id = 3;
string account_type = 4;
string currency = 5;
double balance = 6;
string status = 7;
string created_at = 8;
}
message UpdateAccountRequest {
string account_id = 1;
string status = 2;
}
message UpdateAccountResponse {
string message = 1;
GetAccountResponse account = 2;
}
message DeleteAccountRequest {
string account_id = 1;
}
message DeleteAccountResponse {
string message = 1;
}
message ListAccountRequest {
string customer_id = 1;
int32 page_number = 2;
int32 page_size = 3;
}
message ListAccountResponse {
repeated GetAccountResponse accounts = 1;
int32 total_count = 2;
}
Protocol Buffer Structure Breakdown
- Syntax Declaration:
syntax = "proto3"specifies Protocol Buffers version 3 - Package:
package grpc_cruddefines the namespace - Go Package Option: Specifies where generated Go code will be placed
- Service Definition: Contains all RPC method signatures
- Messages: Request/response pairs with uniquely numbered fields
Generating Go Code
Create the pb/ directory, then compile the proto file:
mkdir pb
protoc \
-I proto \
--go_out=pb \
--go_opt=paths=source_relative \
--go-grpc_out=pb \
--go-grpc_opt=paths=source_relative \
proto/*.proto
What this does:
I proto: Specifies the input directory-go_out=pb: Generates message structs in thepbdirectory-go-grpc_out=pb: Generates gRPC service codepaths=source_relative: Keeps generated files in the same relative structure
This generates two files in pb/:
user_account.pb.go: Contains message type definitionsuser_account_grpc.pb.go: Contains service interfaces and client/server code
Update dependencies:
go mod tidy
Database Models
Create models/user.go:
package models
import (
"time"
"gorm.io/gorm"
)
type Customer struct {
ID string `gorm:"primaryKey;column:customer_id"`
FirstName string `gorm:"not null"`
LastName string `gorm:"not null"`
Address string
Email string `gorm:"not null;uniqueIndex"`
Phone string `gorm:"not null;uniqueIndex"`
Accounts []Account `gorm:"foreignKey:CustomerID;constraint:OnUpdate:CASCADE,OnDelete:RESTRICT"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt gorm.DeletedAt `gorm:"index"`
}
func (Customer) TableName() string {
return "customers"
}
type Account struct {
ID string `gorm:"primaryKey;column:account_id"`
AccountNumber string `gorm:"uniqueIndex;not null"`
Status string `gorm:"type:varchar(20);not null"` // active, frozen, closed
Balance float64 `gorm:"type:numeric(18,2);not null;default:0"`
OpenedAt time.Time `gorm:"not null"`
CustomerID string `gorm:"not null"`
Customer Customer `gorm:"foreignKey:CustomerID"`
Currency string `gorm:"type:varchar(3);not null;default:'NPR'"`
AccountType string `gorm:"type:varchar(20);not null;default:'savings'"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt gorm.DeletedAt `gorm:"index"`
}
func (Account) TableName() string {
return "accounts"
}
Key Features:
- UUID-based primary keys for distributed system compatibility
- Foreign key relationships with cascade rules
- Soft deletes using GORM’s
DeletedAtfield - Unique constraints on email and phone numbers
- Precise decimal handling for currency (numeric 18,2)
Implementing the Service
Create service/account_service.go:
package service
import (
"context"
"time"
"github.com/google/uuid"
pb "github.com/paudelanil/grpc-crud/pb"
"github.com/paudelanil/grpc-crud/models"
"gorm.io/gorm"
)
type AccountService struct {
pb.UnimplementedAccountServiceServer
DB *gorm.DB
}
func (s *AccountService) CreateUser(ctx context.Context, req *pb.CreateCustomerRequest) (*pb.CreateCustomerResponse, error) {
customer := models.Customer{
ID: uuid.New().String(),
FirstName: req.FirstName,
LastName: req.LastName,
Email: req.Email,
Address: req.Address,
Phone: req.PhoneNumber,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
result := s.DB.Create(&customer)
if result.Error != nil {
return nil, result.Error
}
return &pb.CreateCustomerResponse{
CustomerId: customer.ID,
Message: "Customer created successfully",
}, nil
}
Implementation Details:
- Embeds
UnimplementedAccountServiceServerfor forward compatibility - Uses dependency injection for database access
- Generates UUIDs for customer IDs
- Returns gRPC errors for database failures
Setting Up the Server
Create cmd/server/main.go:
package main
import (
"fmt"
"log"
"net"
pb "github.com/paudelanil/grpc-crud/pb"
"github.com/paudelanil/grpc-crud/models"
"github.com/paudelanil/grpc-crud/service"
"google.golang.org/grpc"
"google.golang.org/grpc/reflection"
"gorm.io/driver/postgres"
"gorm.io/gorm"
)
func main() {
// Database connection
dsn := "host=localhost user=postgres password=pass dbname=grpc_crud port=5432 sslmode=disable"
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{
DisableForeignKeyConstraintWhenMigrating: true,
})
if err != nil {
log.Fatalf("Failed to connect to database: %v", err)
}
// Auto-migrate database schema
if err := db.AutoMigrate(&models.Customer{}, &models.Account{}); err != nil {
log.Fatalf("Failed to migrate database: %v", err)
}
// Start gRPC server
lis, err := net.Listen("tcp", fmt.Sprintf("%s:%s", "localhost", "8090"))
if err != nil {
log.Fatalf("Failed to listen: %v", err)
}
grpcServer := grpc.NewServer()
accountService := &service.AccountService{DB: db}
pb.RegisterAccountServiceServer(grpcServer, accountService)
// Enable reflection for tools like Postman
reflection.Register(grpcServer)
log.Println("gRPC server listening on port 8090")
if err := grpcServer.Serve(lis); err != nil {
log.Fatalf("Failed to serve: %v", err)
}
}
Server Setup Steps:
- Establish PostgreSQL connection using GORM
- Run automatic migrations to create/update tables
- Create TCP listener on port 8090
- Initialize gRPC server
- Register service implementation
- Enable server reflection for debugging tools
- Start serving requests
Testing with Postman
The reflection.Register(grpcServer) line enables gRPC server reflection, which allows tools like Postman to discover available services and methods dynamically.
Testing Steps:
- Create New gRPC Request in Postman
- Select “New” → “gRPC Request”
- Enter server URL:
localhost:8090
- Select Method
- Choose
grpc_crud.AccountService/CreateUser - Postman will automatically load the method signature
- Choose
-
Send Request
{ "first_name": "John", "last_name": "Doe", "email": "[email protected]", "phone_number": "+1234567890", "address": "123 Main St, City, Country" } -
Verify Response
{ "customer_id": "550e8400-e29b-41d4-a716-446655440000", "message": "Customer created successfully" }
Database Verification
You can verify the created record using any PostgreSQL client (pgAdmin, DBeaver, or psql):
SELECT * FROM customers WHERE customer_id = '550e8400-e29b-41d4-a716-446655440000';
What We’ve Accomplished
In this first part, we’ve successfully:
✅ Defined service contracts using Protocol Buffers
✅ Generated Go code from proto definitions
✅ Implemented the CreateUser RPC method
✅ Set up PostgreSQL with GORM ORM
✅ Created database models with proper relationships
✅ Launched a working gRPC server
✅ Tested the service using Postman
Next Steps
In the upcoming parts of this series, we’ll cover:
- Part 2: Implementing all CRUD operations for Customer and Account entities
- Part 3: Transaction management for bank operations
- Part 4: Adding authentication and authorization with JWT tokens
- Part 5: Implementing gRPC interceptors for middleware functionality
Running the Project
# Start PostgreSQL (using Docker)
docker run --name grpc-postgres \
-e POSTGRES_PASSWORD=pass \
-e POSTGRES_DB=grpc_crud \
-p 5432:5432 \
-d postgres
# Run the server
go run cmd/server/main.go
Conclusion
This guide demonstrates the fundamentals of building a gRPC service in Go. By combining Protocol Buffers for efficient serialization, GORM for database operations, and gRPC for communication, we’ve created a solid foundation for a scalable banking application.
The modular structure we’ve established makes it easy to extend functionality, add new services, and implement additional features like authentication and transaction processing in future iterations.
Stay tuned for Part 2, where we’ll implement the remaining CRUD operations and explore best practices for error handling in gRPC services.