Implementing User Service with gRPC and PostgreSQL - Part 3

In Part 2 we introduced apis for User service, this post walks through implementing a User Service in Go using gRPC with PostgreSQL as the database. The service provides user authentication with JWT-based authentication. The implementation includes:
gRPC service definition
Database integration using GORM
JWT-based authentication
A structured service layer
Note: Please replace the github user name and repo in each of the following folder
Project Structure
The folder structure for the service is as follows:
ecom-grpc/userd/
│-- db/
│ │-- db.go
│ │-- user.go
│-- service/
│ │-- service.go
│ │-- login.go
│ │-- register.go
│ │-- me.go
│-- utils/
│ │-- jwt.go
│-- main.go
│-- .env
│-- Dockerfile
│-- .dockerignore
Database Provider (db/db.go
)
This file defines the interface for database operations and initializes the connection to PostgreSQL using GORM.
package db
import (
"log"
"gorm.io/driver/postgres"
"gorm.io/gorm"
)
// Provider defines the interface for the database provider
type Provider interface {
CreateUser(user *User) (*User, error)
GetUserByEmail(email string) (*User, error)
GetUserByID(id string) (*User, error)
}
// provider implements the Provider interface
type provider struct {
db *gorm.DB
}
// New creates new database provider
// connects to db and returns the provider
func New(dbURL string) Provider {
db, err := gorm.Open(postgres.Open(dbURL), &gorm.Config{})
if err != nil {
log.Fatalf("Failed to connect to database: %v", err)
}
// Auto-migrate User model
db.AutoMigrate(&User{})
return &provider{db}
}
Database User Methods (db/user.go
)
This file implements the methods to interact with the users
table.
package db
import (
"github.com/google/uuid"
"golang.org/x/crypto/bcrypt"
"gorm.io/gorm"
user "github.com/lakhansamani/ecom-grpc-apis/user/v1"
)
// User represents the User model in DB
type User struct {
ID string `gorm:"primaryKey"`
Name string
Email string `gorm:"unique"`
Password string
}
// AsAPIUser converts the User model to API User
func (u *User) AsAPIUser() *user.User {
return &user.User{
Id: u.ID,
Name: u.Name,
Email: u.Email,
}
}
// BeforeSave GORM hook to hash password only if it's changed
func (u *User) BeforeSave(tx *gorm.DB) (err error) {
// Hash the new password
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(u.Password), bcrypt.DefaultCost)
if err != nil {
return err
}
u.Password = string(hashedPassword)
return nil
}
// Before create
func (u *User) BeforeCreate(tx *gorm.DB) (err error) {
// Generate UUID
u.ID = uuid.NewString()
return
}
// CreateUser creates a new user in the database
func (p *provider) CreateUser(u *User) (*User, error) {
err := p.db.Create(u).Error
return u, err
}
// GetUserByEmail fetches a user by email from the database
func (p *provider) GetUserByEmail(email string) (*User, error) {
var u User
err := p.db.Where("email = ?", email).First(&u).Error
return &u, err
}
// GetUserByID fetches a user by ID from the database
func (p *provider) GetUserByID(id string) (*User, error) {
var u User
err := p.db.Where("id = ?", id).First(&u).Error
return &u, err
}
User Service (service/service.go
)
This file defines the service layer for user operations. It holds configurations and dependencies.
package service
import (
user "github.com/lakhansamani/ecom-grpc-apis/user/v1"
"github.com/lakhansamani/ecom-grpc-userd/db"
)
type Config struct {
JWTSecret string
}
type Dependencies struct {
DBProvider db.Provider
}
// Service implements the User service.
type Service interface {
user.UserServiceServer
}
type service struct {
Config
Dependencies
}
// New creates a new User service.
func New(cfg Config, deps Dependencies) Service {
return &service{
Config: cfg,
Dependencies: deps,
}
}
Register API (service/register.go
)
Handles user registration.
package service
import (
"context"
"errors"
"strings"
user "github.com/lakhansamani/ecom-grpc-apis/user/v1"
"github.com/lakhansamani/ecom-grpc-userd/db"
)
// Register API to register a new user
// Permission: none
func (s *service) Register(ctx context.Context, req *user.RegisterRequest) (*user.RegisterResponse, error) {
name := req.GetName()
email := req.GetEmail()
password := req.GetPassword()
if strings.TrimSpace(name) == "" {
return nil, errors.New("name is required")
}
if strings.TrimSpace(email) == "" {
return nil, errors.New("email is required")
}
if strings.TrimSpace(password) == "" {
return nil, errors.New("password is required")
}
resUser, err := s.DBProvider.CreateUser(&db.User{
Name: name,
Email: email,
Password: password,
})
if err != nil {
return nil, err
}
return &user.RegisterResponse{
UserId: resUser.ID,
}, nil
}
Login API (service/login.go
)
Handles user login and JWT generation.
package service
import (
"context"
"errors"
"strings"
"golang.org/x/crypto/bcrypt"
user "github.com/lakhansamani/ecom-grpc-apis/user/v1"
"github.com/lakhansamani/ecom-grpc-userd/utils"
)
// Login API to login a user
// Permission: none
func (s *service) Login(ctx context.Context, req *user.LoginRequest) (*user.LoginResponse, error) {
email := req.GetEmail()
password := req.GetPassword()
if strings.TrimSpace(email) == "" {
return nil, errors.New("email is required")
}
if strings.TrimSpace(password) == "" {
return nil, errors.New("password is required")
}
// Get user by email
resUser, err := s.DBProvider.GetUserByEmail(email)
if err != nil {
return nil, err
}
// Match password
if err := bcrypt.CompareHashAndPassword([]byte(resUser.Password), []byte(password)); err != nil {
return nil, errors.New("invalid password")
}
// Generate JWT token
token, err := utils.GenerateJWT(s.JWTSecret, resUser.ID)
if err != nil {
return nil, err
}
return &user.LoginResponse{
Token: token,
}, nil
}
Me API (service/me.go
)
Retrieves the currently authenticated user.
package service
import (
"context"
"errors"
"strings"
user "github.com/lakhansamani/ecom-grpc-apis/user/v1"
"github.com/lakhansamani/ecom-grpc-userd/utils"
"google.golang.org/grpc/metadata"
)
// Me API to get user details
// Permission: authenticated user
func (s *service) Me(ctx context.Context, req *user.MeRequest) (*user.MeResponse, error) {
// Get the Authorization bearer token from the context
// Extract the token from the header
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return nil, errors.New("missing metadata")
}
authHeader, exists := md["authorization"]
if !exists || len(authHeader) == 0 {
return nil, errors.New("missing authorization token")
}
token := authHeader[0]
// Make sure the token is not empty and is bearer token
if token == "" {
return nil, errors.New("missing token")
}
tokenSplit := strings.Split(token, " ")
if len(tokenSplit) != 2 {
return nil, errors.New("invalid token")
}
if strings.ToLower(tokenSplit[0]) != "bearer" {
return nil, errors.New("invalid token")
}
token = tokenSplit[1]
userID, err := utils.VerifyJWT(s.JWTSecret, token)
if err != nil {
return nil, err
}
// Fetch the user from the database
resUser, err := s.DBProvider.GetUserByID(userID)
if err != nil {
return nil, err
}
// Return the user details
return &user.MeResponse{
User: resUser.AsAPIUser(),
}, nil
}
JWT Utility (utils/jwt.go
)
Handles JWT generation and verification.
package utils
import (
"time"
"github.com/golang-jwt/jwt"
)
// Generate JWT token
func GenerateJWT(secret, userID string) (string, error) {
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"user_id": userID,
"exp": time.Now().Add(time.Hour * 24).Unix(),
})
return token.SignedString([]byte(secret))
}
// VerifyJWT verifies the JWT token
func VerifyJWT(secret, tokenString string) (string, error) {
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
return []byte(secret), nil
})
if err != nil {
return "", err
}
claims, ok := token.Claims.(jwt.MapClaims)
if !ok {
return "", err
}
// Check if token is valid
if !token.Valid {
return "", err
}
return claims["user_id"].(string), nil
}
Main File (main.go
)
Starts the gRPC server and initializes dependencies.
package main
import (
"log"
"net"
"os"
"github.com/joho/godotenv"
"google.golang.org/grpc"
userpb "github.com/lakhansamani/ecom-grpc-apis/user/v1"
"github.com/lakhansamani/ecom-grpc-userd/db"
"github.com/lakhansamani/ecom-grpc-userd/service"
)
func main() {
// Read .env file as environment variables
err := godotenv.Load()
if err != nil {
log.Println(".env file not found, using environment variables")
}
// DB URL
dbURL := os.Getenv("DB_URL")
if dbURL == "" {
log.Fatal("DB_URL is required")
}
// JWT Secret
jwtSecret := os.Getenv("JWT_SECRET")
if jwtSecret == "" {
log.Fatal("JWT_SECRET is required")
}
// Initialize database
dbProvider := db.New(dbURL)
// Create a new gRPC server
server := grpc.NewServer()
// Register UserService with gRPC
userService := service.New(
service.Config{
JWTSecret: jwtSecret,
},
service.Dependencies{
DBProvider: dbProvider,
})
userpb.RegisterUserServiceServer(server, userService)
// Start gRPC server
listener, err := net.Listen("tcp", ":50051")
if err != nil {
log.Fatalf("Failed to listen: %v", err)
}
log.Println("gRPC Server is running on port 50051...")
if err := server.Serve(listener); err != nil {
log.Fatalf("Failed to serve: %v", err)
}
}
Dockerfile (Dockerfile)
This file helps in creating userd
container image that we can use in future with kubernetes.
# Build Stage
FROM golang:1.23 AS builder
WORKDIR /app
# Copy go.mod and go.sum and download dependencies
COPY go.mod go.sum ./
RUN go mod download
# Copy source code
COPY . .
# Build the Go binary with static linking (Alpine compatible)
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o userd ./main.go
# Final Runtime Stage (Alpine)
FROM alpine:latest
WORKDIR /app
# Install certificates (required for HTTPS calls)
RUN apk add --no-cache ca-certificates
# Copy binary from builder
COPY --from=builder /app/userd .
# Expose gRPC port
EXPOSE 50051
# Run the application
CMD ["./userd"]
Docker ignore file (.dockerignore)
This file helps ignoring development files example .env in production build
.env
Here is .env
file for local development
DB_URL=postgres://postgres:postgres@localhost:5432/userdb
JWT_SECRET=secret
Running the Service
docker run --name postgres-cluster -e POSTGRES_PASSWORD=postgres -p 5432:5432 -d postgres
docker exec -it postgres-cluster psql -U postgres -c "CREATE DATABASE userdb;"
go run main.go
Now, your User Service is live with gRPC, PostgreSQL, and JWT authentication! 🚀
You can try following commands with correct .proto
file path
grpcurl -plaintext -d '{ "name": "John Doe", "email": "john@example.com", "password": "securepass" }' -proto=apis/user/v1/user.proto localhost:50051 user.v1.UserService/Register
grpcurl -plaintext -d '{ "email": "john@example.com", "password": "securepass" }' -proto=apis/user/v1/user.proto localhost:50051 user.v1.UserService/Login
grpcurl -plaintext -H "authorization: bearer JWT_TOKEN" -proto=apis/user/v1/user.proto localhost:50051 user.v1.UserService/Me
Code Link
🎯 Next Steps
🚀 Now that we have the User Service implemented, in Part 4, we will:
✅ Implement Order Service.
Subscribe to my newsletter
Read articles from Lakhan Samani directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Lakhan Samani
Lakhan Samani
I’m Lakhan Samani from India 🇮🇳. I’m the creator and maintainer of authorizer.dev | Freelance Software Engineer | Prev: Cloud Software Engineer at ArangoDB. I am passionate about building products and developer tools. This website is my internet space, where I write about Building Software | Open Source | Finance and Life.