Repository navigation
03‐ Refactoring User Service
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:

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.
-
Checkout Branch:
git checkout 02-refactor-user-service
-
Project Structure:
- Let's organize our code into the following structure within the
user-servicedirectory: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
- Let's organize our code into the following structure within the
-
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! }
-
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 }
-
-
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) }
- Create the handler functions to handle HTTP requests:
-
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.