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

  1. Syntax Declaration: syntax = "proto3" specifies Protocol Buffers version 3
  2. Package: package grpc_crud defines the namespace
  3. Go Package Option: Specifies where generated Go code will be placed
  4. Service Definition: Contains all RPC method signatures
  5. 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 the pb directory
  • -go-grpc_out=pb: Generates gRPC service code
  • paths=source_relative: Keeps generated files in the same relative structure

This generates two files in pb/:

  • user_account.pb.go: Contains message type definitions
  • user_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 DeletedAt field
  • 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 UnimplementedAccountServiceServer for 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:

  1. Establish PostgreSQL connection using GORM
  2. Run automatic migrations to create/update tables
  3. Create TCP listener on port 8090
  4. Initialize gRPC server
  5. Register service implementation
  6. Enable server reflection for debugging tools
  7. 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:

  1. Create New gRPC Request in Postman
    • Select “New” → “gRPC Request”
    • Enter server URL: localhost:8090
  2. Select Method
    • Choose grpc_crud.AccountService/CreateUser
    • Postman will automatically load the method signature
  1. Send Request

    {
        "first_name": "John",
        "last_name": "Doe",
        "email": "[email protected]",
        "phone_number": "+1234567890",
        "address": "123 Main St, City, Country"
    }
    
  2. 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.