Skip to content

03‐ Refactoring User Service

Rabieh Fashwall edited this page Jan 21, 2025 · 4 revisions

In this step, we'll refactor our basic User service to incorporate more best practices and prepare it for connecting to a database. We'll focus on structuring our code for maintainability and scalability, although we won't introduce NATS or the CQRS pattern just yet.

This is how our service look like now: Base Service

Goals

By the end of this step, you'll have:

  • A restructured User service that follows a layered architecture.
  • A well-defined data model (User).
  • Database interaction logic separated into a repository layer.
  • HTTP handler functions that interact with the repository.

Refactoring Steps

  1. Checkout Branch:

    git checkout 02-refactor-user-service
  2. Project Structure:

    • Let's organize our code into the following structure within the user-service directory:
      user-service/
      ├── internal
      │   ├── handlers    // HTTP request handlers
      │   ├── models      // Data models and structs
      │   ├── repository  // Database interaction logic
      │   └── service     // Business logic 
      ├── cmd
      │   └── main.go      // Main application file
      └── ...              // Other files (e.g., Dockerfile, go.mod)
      
    • Create the folders and files
      cd user-service
      mkdir -p internal/{handlers,models,repository,service} cmd 
      
      touch internal/models/user.go internal/repository/user.go internal/handlers/user.go cmd/main.go
  3. Model Definition (user-service/internal/models/user.go):

    package models
    
    // User represents a user in our system.
    type User struct {
        ID       int    `json:"id"`
        Name     string `json:"name"`
        Email    string `json:"email"`
        Password string `json:"password"` // Note: In a real application, never store passwords in plain text!
    }
  4. Repository Layer (user-service/internal/repository/user.go):

    • Define the interface for database interaction:

      package repository
      
      import (
          "context"
          "github.com/rfashwall/user-service/internal/models"
      )
      
      // UserRepository defines the interface for interacting with the user repository.
      type UserRepository interface {
          CreateUser(ctx context.Context, user *models.User) error
          GetUserByID(ctx context.Context, id int) (*models.User, error)
          GetAllUsers(ctx context.Context) ([]*models.User, error)
          UpdateUser(ctx context.Context, user *models.User) error
          DeleteUser(ctx context.Context, id int) error
      }
    • Create a MySQL implementation (replace placeholders with your database connection details):

      package repository
      
      import (
          "context"
          "database/sql"
          "fmt"
          _ "github.com/go-sql-driver/mysql"
          "github.com/rfashwall/user-service/internal/models"
      )
      
      // MySqlUserRepository implements UserRepository using MySQL.
      type MySqlUserRepository struct {
          DB *sql.DB
      }
      
      // NewMySQLUserRepository creates a new MySQLUserRepository.
      func NewMySQLUserRepository(db *sql.DB) *MySqlUserRepository {
          return &MySqlUserRepository{DB: db}
      }
      
      // CreateUser inserts a new user into the database.
      func (r *MySqlUserRepository) CreateUser(ctx context.Context, user *models.User) error {
          result, err := r.DB.ExecContext(ctx, "INSERT INTO users (name, email, password) VALUES (?, ?, ?)", 
              user.Name, user.Email, user.Password)
          if err != nil {
              return err
          }
      
          // Get the last inserted ID and update the User model
          lastID, err := result.LastInsertId()
          if err != nil {
              return err
          }
          user.ID = int(lastID) 
      
          return nil
      }
      
      // GetUserByID retrieves a user from the database by ID.
      func (r *MySqlUserRepository) GetUserByID(ctx context.Context, id int) (*models.User, error) {
          user := &models.User{}
          err := r.DB.QueryRowContext(ctx, "SELECT id, name, email, password FROM users WHERE id = ?", id).
              Scan(&user.ID, &user.Name, &user.Email, &user.Password)
          if err != nil {
              if err == sql.ErrNoRows {
                  return nil, fmt.Errorf("user not found")
              }
              return nil, err
          }
          return user, nil
      }
      
      // GetAllUsers retrieves all users from the database.
      func (r *MySqlUserRepository) GetAllUsers(ctx context.Context) ([]*models.User, error) {
          rows, err := r.DB.QueryContext(ctx, "SELECT id, name, email, password FROM users")
          if err != nil {
              return nil, err
          }
          defer rows.Close()
      
          users := []*models.User{}
          for rows.Next() {
              user := &models.User{}
              err := rows.Scan(&user.ID, &user.Name, &user.Email, &user.Password)
              if err != nil {
                  return nil, err
              }
              users = append(users, user)
          }
      
          return users, nil
      }
      
      // UpdateUser updates an existing user in the database.
      func (r *MySqlUserRepository) UpdateUser(ctx context.Context, user *models.User) error {
          _, err := r.DB.ExecContext(ctx, "UPDATE users SET name = ?, email = ?, password = ? WHERE id = ?", 
              user.Name, user.Email, user.Password, user.ID)
          return err
      }
      
      // DeleteUser deletes a user from the database.
      func (r *MySqlUserRepository) DeleteUser(ctx context.Context, id int) error {
          _, err := r.DB.ExecContext(ctx, "DELETE FROM users WHERE id = ?", id)
          return err
      }
  5. Handler Layer (user-service/internal/handlers/user.go):

    • Create the handler functions to handle HTTP requests:
      package handlers
      
      import (
          "github.com/gofiber/fiber/v2"
          "github.com/rfashwall/user-service/internal/models"
          "github.com/rfashwall/user-service/internal/repository"
          "go.uber.org/zap"
      )
      
      // UserHandler handles HTTP requests related to users.
      type UserHandler struct {
          Repo   repository.UserRepository
          Logger *zap.Logger
      }
      
      // NewUserHandler creates a new UserHandler.
      func NewUserHandler(repo repository.UserRepository, logger *zap.Logger) *UserHandler {
          return &UserHandler{
              Repo:   repo,
              Logger: logger,
          }
      }
      
      // CreateUser handles the POST request to create a new user.
      func (h *UserHandler) CreateUser(c *fiber.Ctx) error {
          user := &models.User{}
      
          if err := c.BodyParser(user); err != nil {
              h.Logger.Error("Error parsing request body", zap.Error(err))
              return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"message": "Invalid request body"})
          }
      
          if err := h.Repo.CreateUser(c.Context(), user); err != nil {
              h.Logger.Error("Error creating user", zap.Error(err))
              return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"message": "Failed to create user"})
          }
      
          return c.Status(fiber.StatusCreated).JSON(user)
      }
      
      // GetUserByID handles the GET request to retrieve a user by ID.
      func (h *UserHandler) GetUserByID(c *fiber.Ctx) error {
          id, err := c.ParamsInt("id")
          if err != nil {
              return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"message": "Invalid user ID"})
          }
      
          user, err := h.Repo.GetUserByID(c.Context(), id)
          if err != nil {
              if err.Error() == "user not found" {
                  return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"message": err.Error()})
              }
              return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"message": "Failed to get user"})
          }
      
          return c.JSON(user)
      }
      
      // GetAllUsers handles the GET request to retrieve all users.
      func (h *UserHandler) GetAllUsers(c *fiber.Ctx) error {
          users, err := h.Repo.GetAllUsers(c.Context())
          if err != nil {
              return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"message": "Failed to get users"})
          }
          return c.JSON(users)
      }
      
      // UpdateUser handles the PUT request to update an existing user.
      func (h *UserHandler) UpdateUser(c *fiber.Ctx) error {
          id, err := c.ParamsInt("id")
          if err != nil {
              return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"message": "Invalid user ID"})
          }
      
          user := &models.User{}
          if err := c.BodyParser(user); err != nil {
              return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"message": "Invalid request body"})
          }
      
          user.ID = id
      
          if err := h.Repo.UpdateUser(c.Context(), user); err != nil {
              return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"message": "Failed to update user"})
          }
      
          return c.JSON(user)
      }
      
      // DeleteUser handles the DELETE request to delete a user.
      func (h *UserHandler) DeleteUser(c *fiber.Ctx) error {
          id, err := c.ParamsInt("id")
          if err != nil {
              return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"message": "Invalid user ID"})
          }
      
          if err := h.Repo.DeleteUser(c.Context(), id); err != nil {
              return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"message": "Failed to delete user"})
          }
      
          return c.SendStatus(fiber.StatusNoContent)
      }
  6. Main Application (user-service/cmd/main.go):

    • Initialize the database connection, create repository and handler instances, set up routes, and start the server:

      package main
      
      import (
          "fmt"
          "log"
      
          "github.com/gofiber/fiber/v2"
          "github.com/gofiber/fiber/v2/middleware/logger"
          _ "github.com/go-sql-driver/mysql"
          "github.com/rfashwall/user-service/internal/handlers"
          "github.com/rfashwall/user-service/internal/repository"
          "go.uber.org/zap"
      )
      
      func main() {
          // Database Configuration
          dbHost := "localhost"
          dbPort := "3306"
          dbUser := "root"
          dbPass := "root"
          dbName := "user_tracker"
      
          // Construct the connection string
          connStr := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s", dbUser, dbPass, dbHost, dbPort, dbName)
      
          // Connect to the database
          db, err := sql.Open("mysql", connStr)
          if err != nil {
              log.Fatal(err)
          }
          defer db.Close()
      
          // Check the database connection
          err = db.Ping()
          if err != nil {
              log.Fatal(err)
          }
      
          // Initialize Logger
          zapLogger, _ := zap.NewProduction() 
          defer zapLogger.Sync() // flushes buffer, if any
      
          // Initialize Repository
          userRepo := repository.NewMySQLUserRepository(db)
      
          // Initialize Handler
          userHandler := handlers.NewUserHandler(userRepo, zapLogger)
      
          // Initialize Fiber App
          app := fiber.New()
      
          // Middleware 
          app.Use(logger.New()) // Default Fiber logger
      
          // Routes
          app.Post("/users", userHandler.CreateUser)
          app.Get("/users/:id", userHandler.GetUserByID)
          app.Get("/users", userHandler.GetAllUsers)
          app.Put("/users/:id", userHandler.UpdateUser)
          app.Delete("/users/:id", userHandler.DeleteUser)
      
          // Start the server
          log.Fatal(app.Listen(":3000"))
      }

We have successfully refactored User service with a layered architecture, separating concerns into models, repositories, and handlers.

Next we will refactor to pass variables instead of adding them plain in the code and dockrise it for testing.

Clone this wiki locally