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.