Introduction to CRUD gRPC API Framework
CRUD is an acronym that refers to operations that are considered necessary to implement in a persistent storage application: Create, Read, Update and Delete. On the other hand, gRPC is an open source Remote Procedure Call(RPC) framework that can run in any environment. CRUD and gRPC can be used together to build scalable and robust applications.
There are many resources online that teach you how to build RESTful applications that make use of CRUD.In this article, we are going to learn how to build CRUD applications together with gRPC and PostgreSQL as the database. Below are the critical advantages that gRPC APIs have over RESTFul APIs.
- gRPC offers three types of data streaming namely : client-side, server-side and bidirectional streaming
- 7 to 10 times faster message transmission
- gRPC uses HTTP 2 instead of HTTP 1.1
In this article, we are going to build a CRUD application using Go as the runtime, gRPC for data exchange and PostgreSQL DB for data storage. The application will be made up of three major parts namely server, client and proto. These parts will be discussed in length later in the articles.
Prerequisite
Go programming language installed. One of the three latest releases should be fine
Protocol buffer compiler version 3
Go plugins for protocol compiler. Use the below commands to install compiler plugins.
go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.28
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.2
Update your PATH so that protoc compiler can find the plugins
export PATH="$PATH:$(go env GOPATH)/bin"
Code editor of your choice. I will use Visual Studio code.
Postman
Familiarity with Gin RESTful framework
gRPC architecture
gRPC uses the client and server architecture, where the client application can directly make request/call functions on a server. Unlike RESTful architecture where a client makes a request and waits for a response, in gRPC , that client calls a function on the server application hosted in a different machine as if it were in a local object.
gRPC is based around the idea of defining a service on the server side that has methods that can be called remotely with their parameters and return types. On the other hand the client side has a stub that provides the same methods as the one in the server side.

gRPC uses protocol buffers by default, which are basically open source
mechanisms for serializing structured data. When working with protocol
buffers you first of all define the structure of the data you want to
serialize in a file with a .proto extension as we will see later in
this tutorial.
When you are done defining the structured data, you use a protocol
buffer compiler called protoc to generate client and server code.
The order of application development will be ,
- Write protocol buffer data and compile using protoc compiler
- Write server side code
- Write Client side code
Application structure
As mentioned before, this application will be made up of three major parts(modules):
- proto: All our gRPC generated code will be hosted here. When working with gRPC , the gRPC compiler generates the client and server code based on the content of a .proto file.
- Server side: This module will host code for implementing server side code from the proto module. We also add postgres database connection and operations here.We will also have code that responds to client requests.
- Client side: In this module we host code for making requests to the server side.
Please note that the server and client modules will both have a main.go file and will be run in separate terminals.Follow the below steps for building our application structure.
Create working directory and navigate into it using the terminal
mkdir go-grpc-crud-api && cd go-grpc-crud-api
Create module folders
mkdir proto server client
Initialize go module
go mod init example.com/go-grpc-crud-api
Install dependencies
go get -u google.golang.org/grpc
go get -u gorm.io/gorm
go get –u gorm.io/driver/postgres
go get -u github.com/google/uuid
go get -u github.com/gin-gonic/gin
Protocol buffers
gRPC uses protocol buffers by default and the first step when working
with protocol buffers is to define the structure of the data you want to
serialize in a proto file. The proto file lives in a proto directory.
In this section , we are going to create a proto file with the
structured data that we want to work with. In this article, we are going
to use movies as our data. Each movie will have an id, title and
genre.
Move into the the proto directory and create a new file
called movie.proto.Add the below content into the proto file.
proto/movie.proto
proto/movie.proto
syntax="proto3";
package proto;
option go_package="example.com/go-grpc-crud";
message Movie {
string id =1;
string title =2;
string genre =3;
}
message CreateMovieRequest {
Movie movie = 1;
}
message CreateMovieResponse {
Movie movie = 1;
}
message ReadMovieRequest{
string id =1;
}
message ReadMovieResponse{
Movie movie =1;
}
message ReadMoviesRequest{
}
message ReadMoviesResponse{
repeated Movie movies =1;
}
message UpdateMovieRequest{
Movie movie =1;
}
message UpdateMovieResponse{
Movie movie =1;
}
message DeleteMovieRequest{
string id =1;
}
message DeleteMovieResponse{
bool success =1;
}
service MovieService {
rpc CreateMovie(CreateMovieRequest) returns (CreateMovieResponse) {}
rpc GetMovie(ReadMovieRequest) returns (ReadMovieResponse) {}
rpc GetMovies(ReadMoviesRequest) returns (ReadMoviesResponse) {}
rpc UpdateMovie(UpdateMovieRequest) returns (UpdateMovieResponse) {}
rpc DeleteMovie(DeleteMovieRequest) returns (DeleteMovieResponse) {}
}
Let’s break down the first three statements in the movie.proto file.
syntax=”proto3”;: Defines the syntax for the protocol buffer code. There is proto2 syntax as well. In this case we are using proto3 syntaxpackage proto;:Declares the package in use.This prevents naming conflict between different projects.Option go_package=”example.com/go-grpc-crud-api”;: This option defines the import path of the package that will contain all the generated code for this file.
In protocol buffers data is structured as a message which is a logical
record of information containing a series of name-value pairs called
fields. To define a message data structure, use the keyword message
followed by the name of the message then end with {}.
In our proto file the first message is the Movie message
(message Movie{}) with an id title and genre as its fields. This
Movie message defines the data we are going to interact with. We also
define other messages namely CreateMovieRequest{},
CreateMovieResponse{} etc. Please note that some messages have a movie
as their fields and some have id as their fields for example
ReadMovieRequest. Just keep in mind that a message can be empty or can
contain fields.
One unique message is the ReadMoviesResponse that has a repeated
Movie field(repeated Movie movies =1;). This is how you define a
response that will return an array of values.
Apart from a message, protocol buffer files have a service object as
well. To define a service start with a keyword service followed by the
name of the service then follow the name with {}.A service refines the
methods that will be implemented by the server. These methods will also
be called directly by the client. To define a method, use the keyword
rpc followed by the name of the method name ,argument list (the round
brackets) a returns keyword and lastly curly {} brackets. For example
in our case
rcp CreateMovie(CreateMovieRequest) returns (CreateMovieResponse) {}.
Generate Client and Server code using proto file
Now that the protocol buffer code is ready, we can use it to generate server and client code. This is the stage where we will use the protoc compiler .Navigate to the root directory at the same level as the proto, server and client folder in the terminal. Use the below instruction in the terminal to generate server and client code.
protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative proto/movie.proto
The above command basically means to generate server and client code
using the proto file located in the proto/movie.proto path. The
generated server and client code will be added in the proto folder as
proto.pb.go and proto_grpc.pb.go
Configure gRPC Server
Now that we have the server code ready, we need to write code that
implements rcp methods defined in our MovieService in the movie.proto
file. We also write code that ,
- Creates database model and database connection
- Create gRPC server
- Implement RPC methods
In the server folder, create a main.go file that will host the server
side code.
Create database model and database connection
server/main.go
package main
import (
"context"
"errors"
"flag"
"fmt"
"log"
"net"
"time"
pb "example.com/grpc-demo/proto"
"github.com/google/uuid"
"google.golang.org/grpc"
"gorm.io/driver/postgres"
"gorm.io/gorm"
)
func init() {
DatabaseConnection()
}
var DB *gorm.DB
var err error
type Movie struct {
ID string `gorm:"primarykey"`
Title string
Genre string
CreatedAt time.Time `gorm:"autoCreateTime:false"`
UpdatedAt time.Time `gorm:"autoUpdateTime:false"`
}
func DatabaseConnection() {
host := "localhost"
port := "5432"
dbName := "postgres"
dbUser := "postgres"
password := "pass1234"
dsn := fmt.Sprintf("host=%s port=%s user=%s dbname=%s password=%s sslmode=disable",
host,
port,
dbUser,
dbName,
password,
)
DB, err = gorm.Open(postgres.Open(dsn), &gorm.Config{})
DB.AutoMigrate(Movie{})
if err != nil {
log.Fatal("Error connecting to the database...", err)
}
fmt.Println("Database connection successful...")
}
This is the start of the main.go file code. We start by importing some packages.
Using the init() function we call the DatabaseConnection() function
which creates a database client for us to use.
Next we define a Movie struct that ID, Title and Genre as its
attributes. This Movie struct will be used to generate a table in the
postgres database. In the DatabaseConnection() function, add dbUser,
dbName and password that match your database configurations.
Create gRPC server
var (
port = flag.Int("port", 50051, "gRPC server port")
)
type server struct {
pb.UnimplementedMovieServiceServer
}
Implement RPC methods
func (*server) CreateMovie(ctx context.Context, req *pb.CreateMovieRequest) (*pb.CreateMovieResponse, error) {
fmt.Println("Create Movie")
movie := req.GetMovie()
movie.Id = uuid.New().String()
data := Movie{
ID: movie.GetId(),
Title: movie.GetTitle(),
Genre: movie.GetGenre(),
}
res := DB.Create(&data)
if res.RowsAffected == 0 {
return nil, errors.New("movie creation unsuccessful")
}
return &pb.CreateMovieResponse{
Movie: &pb.Movie{
Id: movie.GetId(),
Title: movie.GetTitle(),
Genre: movie.GetGenre(),
},
}, nil
}
func (*server) GetMovie(ctx context.Context, req *pb.ReadMovieRequest) (*pb.ReadMovieResponse, error) {
fmt.Println("Read Movie", req.GetId())
var movie Movie
res := DB.Find(&movie, "id = ?", req.GetId())
if res.RowsAffected == 0 {
return nil, errors.New("movie not found")
}
return &pb.ReadMovieResponse{
Movie: &pb.Movie{
Id: movie.ID,
Title: movie.Title,
Genre: movie.Genre,
},
}, nil
}
func (*server) GetMovies(ctx context.Context, req *pb.ReadMoviesRequest) (*pb.ReadMoviesResponse, error) {
fmt.Println("Read Movies")
movies := []*pb.Movie{}
res := DB.Find(&movies)
if res.RowsAffected == 0 {
return nil, errors.New("movie not found")
}
return &pb.ReadMoviesResponse{
Movies: movies,
}, nil
}
func (*server) UpdateMovie(ctx context.Context, req *pb.UpdateMovieRequest) (*pb.UpdateMovieResponse, error) {
fmt.Println("Update Movie")
var movie Movie
reqMovie := req.GetMovie()
res := DB.Model(&movie).Where("id=?", reqMovie.Id).Updates(Movie{Title: reqMovie.Title, Genre: reqMovie.Genre})
if res.RowsAffected == 0 {
return nil, errors.New("movies not found")
}
return &pb.UpdateMovieResponse{
Movie: &pb.Movie{
Id: movie.ID,
Title: movie.Title,
Genre: movie.Genre,
},
}, nil
}
func (*server) DeleteMovie(ctx context.Context, req *pb.DeleteMovieRequest) (*pb.DeleteMovieResponse, error) {
fmt.Println("Delete Movie")
var movie Movie
res := DB.Where("id = ?", req.GetId()).Delete(&movie)
if res.RowsAffected == 0 {
return nil, errors.New("movie not found")
}
return &pb.DeleteMovieResponse{
Success: true,
}, nil
}
Explanation
All these functions are implementation of the movie service. We are
implementing methods defined in movie_grpc.pb.go file. Please take
some time to see what this file holds. Each method takes a
Go context as
a first argument and requests as the seconds arguments.These methods
also return a
response and error.
Create movie
The CreateMovie() is responsible for receiving movie data , storing
that data into the database and returning a response to the client.To
get the request data we use the request object and access the GetMovie()
method. We then assign an unique id using uuid.New().String() syntax.
Next we store the new movie into the database using
res :=DB.Create(&data).
Lastly return a protocol buffer CreateMovieResponse response to the
client after successfully storing the data into the database.
Get a movie
GetMovie() method is responsible for using an ID in the request
payload and searching a movie with the given ID. If a movie is found ,
we return the movie to the client else we return an error. We first of
all start by creating a movie variable using the
var movie Movie statement. Next we search the database for a movie
with an id from the req.GetId(). The statement
res := DB.Find(&movie, “id = ?”, req.GetId()) , searches for the movie
in the database. If the movie is found, we return a protocol buffer
ReadMovieResponse to the client, else return an error.
Get movies
GetMovies() returns all the movies in the database to the client. We
start by initializing a movies array using the movies :=
[]*pb.Movie{} statement. This will get assigned all the movies found
in the database. We then return a protocol buffer ReadMoviesResponse
back to the client.
Update movie
The UpdateMovie() receives data from the client and updates the
existing movie in the database. To get the data to update we use the
reqMovie := req.GetMovie() statement. After a successful update we
return a protocol buffer UpdateMovieResponse to the client.
Delete a movie.
The DeleteMovie() method takes the ID from the request and uses it
to delete the movie from the database. After a successful delete
operation we return protocol buffer DeleteMovieResponse back to the
client.
Starting the gRPC server
The server code is hosted in the main function. In the main function we will set up the gRPC server and run it as a separate entity from the client.
server/main.go
func main() {
fmt.Println("gRPC server running ...")
lis, err := net.Listen("tcp", fmt.Sprintf(":%d", *port))
if err != nil {
log.Fatalf("Failed to listen: %v", err)
}
s := grpc.NewServer()
pb.RegisterMovieServiceServer(s, &server{})
log.Printf("Server listening at %v", lis.Addr())
if err := s.Serve(lis); err != nil {
log.Fatalf("failed to serve : %v", err)
}
}
A gRPC server
requires a
TCP connection that listens on port 50051. The
statement lis, err := net.Listen("tcp", fmt.Sprintf(":%d", *port))
create the connection that the gRPC server needs.
After a successful connection, we create a new gRPC server using the
s := grpc.NewServer() statement. Next we register our server with the
newly created gRPC server using the statement
pb.RegisterMovieServiceServer(s, &server{}) statement.
After a successful registration, we run the server using the
s.Server(lis) statement.
The moment that we have been waiting for is here. The next thing is to run the server from the terminal. Navigate to the root folder and run the below code.
go run server/main.go
Example
$ go run server/main.go
Database connection successful...
gRPC server running ...
2022/12/14 11:56:33 Server listening at [::]:50051
Configure gRPC Client
On the client side of things, we are going to use gRPC to communicate with the server. The request will start from Gin REST API to gRPC client and then finally to the gRPC server. The response will travel back the same way, from the gRPC server, to the gRPC client and finally to the Gin framework. Please note that we are using Gin only for testing via Postman. gRPC does not have bindings for Postman or browser based support.
We start by importing protocol buffer code from the proto module,
together with Gin and gRPC. Next we define the port number for the gRPC
client to use in order to communicate with our gRPC server using the
statement
addr = flag.String("addr", "localhost:50051", "the address to connect to").
We then define a movie struct with the same properties as the movie
struct in the server slide. In the main function, we create a connection
to the server using the statement
conn, err := grpc.Dial(*addr,rpc.WithTransportCredentials(insecure.NewCredentials())).
After a successful connection, we create an instance of our gRCP client
using the statement client := pb.NewMovieServiceClient(conn)**.**The
client will give us access to all the CRUD methods implemented by the
server . For example,
- GetMovies()
- GetMovie()
- CreateMovie()
- UpdateMovie()
- DeleteMovie().
Each method will take a Gin context as the first argument and a pointer
to protocol buffer request that matches the method being called. For
example, GetMovies() method will take &pb.ReadMoviesReques``t as its
second argument.
The next thing to do is to create a Gin router using the statement
r := gin.Default(). The router will be used to router HTTP CRUD
requests to the gRPC client
package main
import (
"flag"
"log"
"net/http"
pb "example.com/go-grpc-crud-api/proto"
"github.com/gin-gonic/gin"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
)
var (
addr = flag.String("addr", "localhost:50051", "the address to connect to")
)
type Movie struct {
ID string `json:"id"`
Title string `json:"Title"`
Genre string `json:"genre"`
}
func main() {
flag.Parse()
conn, err := grpc.Dial(*addr, grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
log.Fatalf("did not connect: %v", err)
}
defer conn.Close()
client := pb.NewMovieServiceClient(conn)
r := gin.Default()
r.GET("/movies", func(ctx *gin.Context) {
res, err := client.GetMovies(ctx, &pb.ReadMoviesRequest{})
if err != nil {
ctx.JSON(http.StatusBadRequest, gin.H{
"error": err,
})
return
}
ctx.JSON(http.StatusOK, gin.H{
"movies": res.Movies,
})
})
r.GET("/movies/:id", func(ctx *gin.Context) {
id := ctx.Param("id")
res, err := client.GetMovie(ctx, &pb.ReadMovieRequest{Id: id})
if err != nil {
ctx.JSON(http.StatusNotFound, gin.H{
"message": err.Error(),
})
return
}
ctx.JSON(http.StatusOK, gin.H{
"movie": res.Movie,
})
})
r.POST("/movies", func(ctx *gin.Context) {
var movie Movie
err := ctx.ShouldBind(&movie)
if err != nil {
ctx.JSON(http.StatusBadRequest, gin.H{
"error": err,
})
return
}
data := &pb.Movie{
Title: movie.Title,
Genre: movie.Genre,
}
res, err := client.CreateMovie(ctx, &pb.CreateMovieRequest{
Movie: data,
})
if err != nil {
ctx.JSON(http.StatusBadRequest, gin.H{
"error": err,
})
return
}
ctx.JSON(http.StatusCreated, gin.H{
"movie": res.Movie,
})
})
r.PUT("/movies/:id", func(ctx *gin.Context) {
var movie Movie
err := ctx.ShouldBind(&movie)
if err != nil {
ctx.JSON(http.StatusBadRequest, gin.H{
"error": err.Error(),
})
return
}
res, err := client.UpdateMovie(ctx, &pb.UpdateMovieRequest{
Movie: &pb.Movie{
Id: movie.ID,
Title: movie.Title,
Genre: movie.Genre,
},
})
if err != nil {
ctx.JSON(http.StatusBadRequest, gin.H{
"error": err.Error(),
})
return
}
ctx.JSON(http.StatusOK, gin.H{
"movie": res.Movie,
})
return
})
r.DELETE("/movies/:id", func(ctx *gin.Context) {
id := ctx.Param("id")
res, err := client.DeleteMovie(ctx, &pb.DeleteMovieRequest{Id: id})
if err != nil {
ctx.JSON(http.StatusBadRequest, gin.H{
"error": err.Error(),
})
return
}
if res.Success == true {
ctx.JSON(http.StatusOK, gin.H{
"message": "Movie deleted successfully",
})
return
} else {
ctx.JSON(http.StatusInternalServerError, gin.H{
"error": "error deleting movie",
})
return
}
})
r.Run(":5000")
}
Executing gRPC Client
After adding the above code in the client/main.go file, run the client by issuing the below project.
$ go run client/main.go
Example
$ go run client/main.go
[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.
[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
- using env: export GIN_MODE=release
- using code: gin.SetMode(gin.ReleaseMode)
[GIN-debug] GET /movies --> main.main.func1 (3 handlers)
[GIN-debug] GET /movies/:id --> main.main.func2 (3 handlers)
[GIN-debug] POST /movies --> main.main.func3 (3 handlers)
[GIN-debug] PUT /movies/:id --> main.main.func4 (3 handlers)
[GIN-debug] DELETE /movies/:id --> main.main.func5 (3 handlers)
[GIN-debug] [WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.
Please check https://pkg.go.dev/github.com/gin-gonic/gin#readme-don-t-trust-all-proxies for details.
[GIN-debug] Listening and serving HTTP on :5000
Tests gRPC CRUD API using Postman
This is the time to test if both the client and server are working as expected. Remember the client and server will be running on different terminals. Ensure that the client and server are running in their own terminal environment before starting these tests.
Create a movie

Read a movie

Read movies

Update movie

Delete movie

Summary
In this article, we learn about gRPC CRUD application, using postgres as the data store and Gin for routing HTTP requests to gRPC clients. We also get to learn about a brief introduction to gRPC architecture that is made up of the client and server. The server connects to the database and responds to the client after every request.


