DEV Community

Lakhan Samani
Lakhan Samani

Posted on

Implementing User Service with go, 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
Enter fullscreen mode Exit fullscreen mode

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}
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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,
    }
}
Enter fullscreen mode Exit fullscreen mode

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
}

Enter fullscreen mode Exit fullscreen mode

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
}

Enter fullscreen mode Exit fullscreen mode

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
}

Enter fullscreen mode Exit fullscreen mode

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
}

Enter fullscreen mode Exit fullscreen mode

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)
    }
}
Enter fullscreen mode Exit fullscreen mode

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"]
Enter fullscreen mode Exit fullscreen mode

Docker ignore file (.dockerignore)

This file helps ignoring development files example .env in production build

.env
Enter fullscreen mode Exit fullscreen mode

Here is .env file for local development

DB_URL=postgres://postgres:postgres@localhost:5432/userdb
JWT_SECRET=secret
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Code Link

🎯 Next Steps

  • 🚀 Now that we have the User Service implemented, in Part 4, we will:
  • ✅ Implement Order Service.

Top comments (0)