[sqlc]gRPC API Development Tutorial
[sqlc]gRPC API Development Tutorial
In this tutorial, we will walk through the steps to develop a simple gRPC API for a memo application using Go. Each step will be explained with its purpose and rationale, following best practices.
Prerequisites
Ensure that the following tools are installed:
- Go
- Protocol Buffers (
protoc
) sqlc
direnv
- PostgreSQL
evans
1. Project Setup
1.1 Create Project Directory
First, create the project directory and navigate into it.
mkdir grpc-memo-api
cd grpc-memo-api
1.2 Initialize Go Module
Initialize the Go module. This helps in managing dependencies.
go mod init grpc-memo-api
1.3 Create Necessary Directories
Create the project directory structure. This helps in organizing the code.
mkdir -p api/proto
mkdir -p internal/server
mkdir -p internal/db/generated
mkdir -p cmd
Directory structure explanation:
-
api/proto
: Contains Protocol Buffers definition files -
internal/server
: Contains gRPC server implementation -
internal/db/generated
: Contains code generated from SQL queries -
cmd
: Contains the main entry point
api/proto/memo.proto
1.4 Create Create the Protocol Buffers definition file. This defines the gRPC interface.
syntax = "proto3"; // Specify the version of Protocol Buffers
package memo; // Specify the package name
option go_package = "grpc-memo-api/api/proto;proto"; // Specify the package name for the generated Go code
// Define the MemoService service
service MemoService {
rpc ListMemos (ListMemosRequest) returns (ListMemosResponse); // RPC to list memos
rpc GetMemo (GetMemoRequest) returns (GetMemoResponse); // RPC to get a specific memo
rpc CreateMemo (CreateMemoRequest) returns (CreateMemoResponse); // RPC to create a new memo
}
// Define the ListMemosRequest message
message ListMemosRequest {
string user_id = 1; // User ID
}
// Define the ListMemosResponse message
message ListMemosResponse {
repeated Memo memos = 1; // List of memos
}
// Define the GetMemoRequest message
message GetMemoRequest {
string user_id = 1; // User ID
string memo_id = 2; // Memo ID
}
// Define the GetMemoResponse message
message GetMemoResponse {
Memo memo = 1; // Memo
}
// Define the CreateMemoRequest message
message CreateMemoRequest {
string user_id = 1; // User ID
string content = 2; // Memo content
}
// Define the CreateMemoResponse message
message CreateMemoResponse {
Memo memo = 1; // Created memo
}
// Define the Memo message
message Memo {
string id = 1; // Memo ID
string user_id = 2; // User ID
string content = 3; // Memo content
string created_at = 4; // Creation timestamp
}
1.5 Compile Protocol Buffers
Run the following command to generate Go code. This generates the gRPC server and client code.
protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative api/proto/memo.proto
This command results in the following folder structure:
.
├── api
│ └── proto
│ ├── memo.proto
│ ├── memo.pb.go
│ └── memo_grpc.pb.go
├── cmd
│ └── main.go
├── go.mod
├── internal
│ ├── db
│ │ ├── db.go
│ │ └── generated
│ └── server
│ └── memo_server.go
└── sqlc.yaml
2. Server Implementation
internal/server/memo_server.go
2.1 Create Implement the gRPC server. This defines the logic to handle client requests.
package server
import (
"context"
"grpc-memo-api/api/proto"
"grpc-memo-api/internal/db/generated"
"time"
"github.com/google/uuid"
)
// MemoServer implements the MemoService.
type MemoServer struct {
proto.UnimplementedMemoServiceServer // Embed to have forward-compatible implementations
DB *generated.Queries // Field to execute database queries
}
// ListMemos lists memos for the specified user.
func (s *MemoServer) ListMemos(ctx context.Context, req *proto.ListMemosRequest) (*proto.ListMemosResponse, error) {
// Retrieve memos from the database
memos, err := s.DB.ListMemos(ctx, req.UserId)
if err != nil {
return nil, err
}
// Convert to Protocol Buffers format
var protoMemos []*proto.Memo
for _, memo := range memos {
protoMemos = append(protoMemos, &proto.Memo{
Id: memo.ID.String(),
UserId: memo.UserID,
Content: memo.Content,
CreatedAt: memo.CreatedAt.Format(time.RFC3339),
})
}
// Return the response
return &proto.ListMemosResponse{Memos: protoMemos}, nil
}
// GetMemo retrieves a memo for the specified user and memo ID.
func (s *MemoServer) GetMemo(ctx context.Context, req *proto.GetMemoRequest) (*proto.GetMemoResponse, error) {
// Retrieve memo from the database
memo, err := s.DB.GetMemo(ctx, generated.GetMemoParams{
UserID: req.UserId,
ID: uuid.MustParse(req.MemoId),
})
if err != nil {
return nil, err
}
// Return the response
return &proto.GetMemoResponse{Memo: &proto.Memo{
Id: memo.ID.String(),
UserId: memo.UserID,
Content: memo.Content,
CreatedAt: memo.CreatedAt.Format(time.RFC3339),
}}, nil
}
// CreateMemo creates a new memo.
func (s *MemoServer) CreateMemo(ctx context.Context, req *proto.CreateMemoRequest) (*proto.CreateMemoResponse, error) {
// Generate new memo ID and creation timestamp
memoID := uuid.New()
createdAt := time.Now()
// Insert memo into the database
params := generated.CreateMemoParams{
ID: memoID,
UserID: req.UserId,
Content: req.Content,
CreatedAt: createdAt,
}
err := s.DB.CreateMemo(ctx, params)
if err != nil {
return nil, err
}
// Return the response
return &proto.CreateMemoResponse{Memo: &proto.Memo{
Id: memoID.String(),
UserId: req.UserId,
Content: req.Content,
CreatedAt: createdAt.Format(time.RFC3339),
}}, nil
}
3. Database Access Implementation
internal/db/db.go
3.1 Create Create a function to initialize the database connection. This establishes the connection to the database.
package db
import (
"database/sql"
_ "github.com/lib/pq" // Import PostgreSQL driver
)
// NewDB initializes the database connection.
func NewDB(dataSourceName string) (*sql.DB, error) {
// Open the database connection
conn, err := sql.Open("postgres", dataSourceName)
if err != nil {
return nil, err
}
return conn, nil
}
4. Main Function Implementation
cmd/main.go
4.1 Create Implement the main function. This starts the gRPC server and listens for client requests.
package main
import (
"os"
"log"
"net"
"grpc-memo-api/api/proto"
"grpc-memo-api/internal/db"
"grpc-memo-api/internal/db/generated"
"grpc-memo-api/internal/server"
"google.golang.org/grpc"
"google.golang.org/grpc/reflection"
"github.com/joho/godotenv"
)
func main() {
// Load .env file
err := godotenv.Load()
if err != nil {
log.Fatalf("Error loading .env file")
}
// Get database URL from environment variables
databaseURL := os.Getenv("DATABASE_URL")
if databaseURL == "" {
log.Fatalf("DATABASE_URL is not set in the environment")
}
// Initialize database connection
conn, err := db.NewDB(databaseURL)
if err != nil {
log.Fatalf("failed to connect to database: %v", err)
}
queries := generated.New(conn)
// Initialize gRPC server
grpcServer := grpc.NewServer()
memoServer := &server.MemoServer{DB: queries}
proto.RegisterMemoServiceServer(grpcServer, memoServer)
reflection.Register(grpcServer)
// Initialize TCP listener
lis, err := net.Listen("tcp", ":50051")
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
log.Println("gRPC server is running on port 50051")
// Start gRPC server
if err := grpcServer.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}
sqlc
Configuration and Execution
5.
sqlc.yaml
5.1 Create Create the sqlc
configuration file. This generates Go code from SQL queries.
version: "2"
sql:
- schema: "schema.sql" # Path to the schema file
queries: "queries.sql" # Path to the query file
engine: "postgresql" # Database engine to use
gen:
go:
package: "generated" # Package name for the generated Go code
out: "internal/db/generated" # Output directory for the generated Go code
schema.sql
5.2 Create Define the database schema. This creates the database tables.
CREATE TABLE memos (
id UUID PRIMARY KEY, // Memo ID
user_id TEXT NOT NULL, // User ID
content TEXT NOT NULL, // Memo content
created_at TIMESTAMP NOT NULL // Creation timestamp
);
queries.sql
5.3 Create Define the SQL queries. This simplifies database operations.
-- name: ListMemos :many
SELECT id, user_id, content, created_at FROM memos WHERE user_id = $1;
-- name: GetMemo :one
SELECT id, user_id, content, created_at FROM memos WHERE user_id = $1 AND id = $2;
-- name: CreateMemo :exec
INSERT INTO memos (id, user_id, content, created_at) VALUES ($1, $2, $3, $4);
sqlc
5.4 Run Run the following command to generate Go code from the SQL queries.
sqlc generate
This command generates the following files:
internal/db/generated/
├── db.go
├── models.go
├── querier.go
├── queries.sql.go
-
db.go
: Contains code for initializing the database connection and managing transactions. -
models.go
: Defines Go structs corresponding to the database tables. -
querier.go
: Defines the query interface. -
queries.sql.go
: Contains Go functions corresponding to the SQL queries defined inqueries.sql
.
These files are used to simplify database access. Specifically, they are used in internal/server/memo_server.go
to perform database operations.
6. Database Setup
6.1 Install PostgreSQL
Install PostgreSQL and start the service.
brew install postgresql
brew services start postgresql@14
6.2 Create Database
Create the database.
createdb memo
6.3 Create Database User and Set Password
Enter the PostgreSQL prompt and create a user and set a password.
psql -d memo
Once in the PostgreSQL prompt, run the following commands:
CREATE USER myuser WITH PASSWORD 'mypassword';
GRANT ALL PRIVILEGES ON DATABASE memo TO myuser;
ALTER DATABASE memo SET timezone TO 'UTC';
schema.sql
6.4 Execute Run the schema file to create the tables.
psql -d memo -U myuser -f schema.sql
7. API Verification
7.1 Start the Server
Start the gRPC server.
go run cmd/main.go
evans
7.2 Install and Use Install evans
and connect to the gRPC server in REPL mode.
brew install evans
evans --host localhost --port 50051 -r repl
evans
7.3 Connect to gRPC Server Using Run the following command to start evans
in REPL mode and connect to the gRPC server.
evans --host localhost --port 50051 -r repl
7.4 Select Package
In the evans
prompt, run the package
command to select the package.
package memo
7.5 Select Service
In the evans
prompt, run the show service
command to display available services.
show service
Select the MemoService
from the displayed services.
service MemoService
7.6 Select Method and Send Request
MemoService
has the following methods:
ListMemos
GetMemo
CreateMemo
Select each method and send a request.
CreateMemo
Select the CreateMemo
method and send a request.
call CreateMemo
When prompted, enter the request data as follows:
{
"user_id": "b7e72808-d71a-4502-a289-35cbafd1352c",
"content": "test"
}
GetMemo
Select the GetMemo
method and send a request.
call GetMemo
When prompted, enter the request data as follows:
{
"user_id": "b7e72808-d71a-4502-a289-35cbafd1352c",
"memo_id": "fdf4fd15-3056-4853-a6ce-ef113db86f5c"
}
ListMemos
Select the ListMemos
method and send a request.
call ListMemos
When prompted, enter the request data as follows:
{
"user_id": "b7e72808-d71a-4502-a289-35cbafd1352c"
}
7.7 Verify Results
After sending requests for each method, the server's response will be displayed. This confirms that the gRPC server is functioning correctly.
Summary
In this tutorial, we walked through the steps to develop a simple gRPC API for a memo application using Go. We covered the following steps to build a gRPC server and integrate it with a database:
- Project setup
- gRPC server implementation
- Database access implementation
- Main function implementation
-
sqlc
configuration and execution - Database setup
- API verification
By following best practices and clearly explaining the purpose and rationale for each step, we can efficiently and effectively develop a gRPC API. This provides a solid foundation for building scalable and maintainable applications.
Final Directory Structure
.
├── api
│ └── proto
│ ├── memo.proto # Protocol Buffers definition file
│ ├── memo.pb.go # Generated Go code
│ └── memo_grpc.pb.go # Generated gRPC server and client code
├── cmd
│ └── main.go # Main entry point
├── go.mod # Go module configuration file
├── internal
│ ├── db
│ │ ├── db.go # Database connection initialization
│ │ └── generated # Code generated from SQL queries
│ │ ├── db.go
│ │ ├── models.go
│ │ ├── querier.go
│ │ └── queries.sql.go
│ └── server
│ └── memo_server.go # gRPC server implementation
└── sqlc.yaml # sqlc configuration file
Following this directory structure helps in organizing the code and makes the project easier to maintain.
Discussion