Building a gRPC API in Go with PostgreSQL

In this tutorial, we'll walk through building a robust gRPC API in Go that interacts with a PostgreSQL database. We'll structure our application by separating models and services, following best practices to create scalable and maintainable code.

Introduction

gRPC is a high-performance, open-source universal RPC framework that uses Protocol Buffers as its interface definition language (IDL) and data serialization format. It allows you to define services and methods, and it generates code for both the client and server.

In this guide, we'll build a gRPC API for managing books, performing CRUD operations with a PostgreSQL database. We'll separate concerns by organizing our code into models and services.

Prerequisites

  • Go installed (Download Go)

  • PostgreSQL installed (Download PostgreSQL)

  • Protocol Buffers Compiler (protoc) installed (Download protoc)

  • Basic knowledge of Go programming

  • A code editor (e.g., VSCode, GoLand)

  • grpcurl or grpcui for testing the API

Project Setup

Create a new directory for your project:

mkdir grpc-bookstore
cd grpc-bookstore

Initialize a new Go module:

go mod init github.com/yourusername/grpc-bookstore

Replace github.com/yourusername/grpc-bookstore with your actual GitHub username or module path.

Installing Dependencies

We'll need the following dependencies:

  1. gRPC-Go: The Go implementation of gRPC

  2. Protocol Buffers: For serialization

  3. GORM: ORM library for Go

  4. pq: PostgreSQL driver for Go

Install them using the following commands:

go get -u google.golang.org/grpc
go get -u google.golang.org/protobuf
go get -u gorm.io/gorm
go get -u gorm.io/driver/postgres

Connecting to PostgreSQL

Setting Up the Database

First, set up the PostgreSQL database.

  1. Create a database named bookstore.

Connect to PostgreSQL and run:

CREATE DATABASE grpcbookstore;
  1. Create a books table.

Connect to the bookstore database and run:

CREATE TABLE grpcbooks (
    id SERIAL PRIMARY KEY,
    title VARCHAR(100),
    author VARCHAR(100),
    description TEXT
);

Configuring the Database Connection

Create a new directory for database configurations:

mkdir database
touch database/database.go

Add the following code to database/database.go:

package database

import (
    "fmt"
    "log"
    "os"

    "gorm.io/driver/postgres"
    "gorm.io/gorm"
)

var DB *gorm.DB

func Connect() {
    var err error
    dsn := "host=localhost user=yourusername password=yourpassword dbname=bookstore port=5432 sslmode=disable TimeZone=Asia/Shanghai"
    DB, err = gorm.Open(postgres.Open(dsn), &gorm.Config{})
    if err != nil {
        log.Fatal("Failed to connect to database:", err)
    }
    fmt.Println("Database connection established")
}

Replace yourusername and yourpassword with your PostgreSQL username and password.

Defining Protobuf Messages and Services

Create a directory for protobuf files:

mkdir proto
touch proto/book.proto

In proto/book.proto, define the messages and services:

syntax = "proto3";

package proto;

option go_package = "github.com/yourusername/grpc-bookstore/proto";

message Book {
  int32 id = 1;
  string title = 2;
  string author = 3;
  string description = 4;
}

message Empty {}

message BookID {
  int32 id = 1;
}

message BookList {
  repeated Book books = 1;
}

service BookService {
  rpc CreateBook(Book) returns (Book);
  rpc GetBook(BookID) returns (Book);
  rpc UpdateBook(Book) returns (Book);
  rpc DeleteBook(BookID) returns (Empty);
  rpc ListBooks(Empty) returns (BookList);
}

Note: Replace github.com/yourusername/grpc-bookstore/proto with your module path.

Generating Go Code from Protobuf Files

You need to generate Go code from the .proto file using the Protocol Buffers compiler (protoc).

First, install the Go plugins for protoc:

go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest

Make sure your $GOPATH/bin is in your $PATH so that protoc can find the plugins.

Generate the code:

protoc --go_out=. --go-grpc_out=. proto/book.proto

This command will generate book.pb.go and book_grpc.pb.go in the proto directory.

Creating Models

Create a new directory for models:

mkdir models
touch models/book.go

In models/book.go, define the Book struct:

package models

type Book struct {
    ID          uint   `json:"id" gorm:"primaryKey"`
    Title       string `json:"title"`
    Author      string `json:"author"`
    Description string `json:"description"`
}

Setting Up Services

Create a directory for services:

mkdir services
touch services/book_service.go

In services/book_service.go, implement the service methods:

package services

import (
    "context"
    "errors"

    "github.com/yourusername/grpc-bookstore/database"
    "github.com/yourusername/grpc-bookstore/models"
    "github.com/yourusername/grpc-bookstore/proto"
    "gorm.io/gorm"
)

type BookServiceServer struct {
    proto.UnimplementedBookServiceServer
}

func (s *BookServiceServer) CreateBook(ctx context.Context, req *proto.Book) (*proto.Book, error) {
    book := models.Book{
        Title:       req.Title,
        Author:      req.Author,
        Description: req.Description,
    }
    if err := database.DB.Create(&book).Error; err != nil {
        return nil, err
    }
    res := &proto.Book{
        Id:          int32(book.ID),
        Title:       book.Title,
        Author:      book.Author,
        Description: book.Description,
    }
    return res, nil
}

func (s *BookServiceServer) GetBook(ctx context.Context, req *proto.BookID) (*proto.Book, error) {
    var book models.Book
    if err := database.DB.First(&book, req.Id).Error; err != nil {
        if errors.Is(err, gorm.ErrRecordNotFound) {
            return nil, errors.New("book not found")
        }
        return nil, err
    }
    res := &proto.Book{
        Id:          int32(book.ID),
        Title:       book.Title,
        Author:      book.Author,
        Description: book.Description,
    }
    return res, nil
}

func (s *BookServiceServer) UpdateBook(ctx context.Context, req *proto.Book) (*proto.Book, error) {
    var book models.Book
    if err := database.DB.First(&book, req.Id).Error; err != nil {
        if errors.Is(err, gorm.ErrRecordNotFound) {
            return nil, errors.New("book not found")
        }
        return nil, err
    }
    book.Title = req.Title
    book.Author = req.Author
    book.Description = req.Description
    if err := database.DB.Save(&book).Error; err != nil {
        return nil, err
    }
    res := &proto.Book{
        Id:          int32(book.ID),
        Title:       book.Title,
        Author:      book.Author,
        Description: book.Description,
    }
    return res, nil
}

func (s *BookServiceServer) DeleteBook(ctx context.Context, req *proto.BookID) (*proto.Empty, error) {
    if err := database.DB.Delete(&models.Book{}, req.Id).Error; err != nil {
        return nil, err
    }
    return &proto.Empty{}, nil
}

func (s *BookServiceServer) ListBooks(ctx context.Context, req *proto.Empty) (*proto.BookList, error) {
    var books []models.Book
    if err := database.DB.Find(&books).Error; err != nil {
        return nil, err
    }
    var res proto.BookList
    for _, book := range books {
        res.Books = append(res.Books, &proto.Book{
            Id:          int32(book.ID),
            Title:       book.Title,
            Author:      book.Author,
            Description: book.Description,
        })
    }
    return &res, nil
}

Note: Replace github.com/yourusername/grpc-bookstore with your module path.

Implementing Server Logic

Create the main server file main.go:

package main

import (
    "fmt"
    "log"
    "net"

    "github.com/yourusername/grpc-bookstore/database"
    "github.com/yourusername/grpc-bookstore/models"
    "github.com/yourusername/grpc-bookstore/proto"
    "github.com/yourusername/grpc-bookstore/services"
    "google.golang.org/grpc"
)

func main() {
    database.Connect()
    database.DB.AutoMigrate(&models.Book{})

    lis, err := net.Listen("tcp", ":50051")
    if err != nil {
        log.Fatalf("Failed to listen: %v", err)
    }

    grpcServer := grpc.NewServer()
    proto.RegisterBookServiceServer(grpcServer, &services.BookServiceServer{})

    fmt.Println("Server is running on port 50051...")
    if err := grpcServer.Serve(lis); err != nil {
        log.Fatalf("Failed to serve: %v", err)
    }
}

Testing the API

Start the Server

Run the application:

go run main.go

Using grpcurl

You can use grpcurl to test the gRPC API. Install grpcurl if you haven't already:

  • For macOS:

      brew install grpcurl
    
  • For other platforms, download from the official releases.

List Available Services

grpcurl -plaintext localhost:50051 list

List Methods in BookService

grpcurl -plaintext localhost:50051 list proto.BookService

Create a New Book

grpcurl -plaintext -d '{
  "title": "The Hitchhiker\'s Guide to the Galaxy",
  "author": "Douglas Adams",
  "description": "A science fiction comedy series."
}' localhost:50051 proto.BookService/CreateBook

Get All Books

grpcurl -plaintext -d '{}' localhost:50051 proto.BookService/ListBooks

Get a Book by ID

grpcurl -plaintext -d '{ "id": 1 }' localhost:50051 proto.BookService/GetBook

Update a Book

grpcurl -plaintext -d '{
  "id": 1,
  "title": "Updated Title",
  "author": "Douglas Adams",
  "description": "An updated description."
}' localhost:50051 proto.BookService/UpdateBook

Delete a Book

grpcurl -plaintext -d '{ "id": 1 }' localhost:50051 proto.BookService/DeleteBook

Conclusion

You've successfully built a gRPC API in Go that interacts with a PostgreSQL database, complete with separated models and services. This architecture is scalable and maintainable, following best practices for building robust applications.

Next Steps

  • Implement Authentication: Use gRPC interceptors to add authentication to your services.

  • Error Handling: Implement detailed error codes and messages using gRPC's status package.

  • Logging and Monitoring: Integrate logging and monitoring tools like Prometheus.

  • Dockerize the Application: Containerize your app for easy deployment.


By following this guide, you've gained valuable experience in building a real-world gRPC API in Go. Keep exploring and enhancing your application!

Happy Coding!