Go: Comprehensive Project: E-commerce API (Part 1)

Last updated: 2026-08-26

That's it for the theory. Now let's get started—building a real e-commerce API from scratch using Go.

Bob decided to start his own e-commerce platform. Three weeks, three stories, and a complete Go واجهة خلفية.

1. You will learn



2. Story: Bob's First Day as an Entrepreneur

(1) Pain point: "I'll build an MVP first and launch it in two weeks."

Bob quit his job to start his own business—an e-commerce platform:

"On the first day, I built a monolith—with all the logic in main.go. By the seventh day, the code had grown to 3,000 lines, and I couldn't tell which parts were order logic and which were user logic. I wanted to add a 'coupon' feature, but after modifying six files, I still wasn't done. My business partner said, 'Launch in two weeks,' but I think even two months might be a stretch."

But after calming down and reflecting on Go's four-layer architecture, he decided to refactor the code:

GO
// Refactoring plan:
// cmd/server/        → entry point
// internal/handler/  → HTTP handlers
// internal/service/  → business logic
// internal/repository/ → data access
// pkg/model/         → domain models

(2) Lesson Objectives: Basic CRUD Operations for Users, Products, and Orders

Week 1 Goal: Build a complete framework and implement CRUD operations for the three core resources.

TEXT 📖 Display only
POST /api/v1/register         → User registration
POST /api/v1/login            → User login
GET  /api/v1/products         → Product list
GET  /api/v1/products/{id}    → Product details
POST /api/v1/orders           → Create order
GET  /api/v1/orders/{id}      → Order details
GET  /api/v1/orders           → My orders list


3. Project Structure

TEXT 📖 Display only
ecommerce/
├── cmd/
│   └── server/
│       └── main.go              # Entry + dependency injection
├── internal/
│   ├── handler/                  # HTTP layer
│   │   ├── user.go
│   │   ├── product.go
│   │   ├── order.go
│   │   └── response.go          # Unified response utilities
│   ├── service/                  # Business layer
│   │   ├── user.go
│   │   ├── product.go
│   │   ├── order.go
│   │   └── errors.go            # Business error definitions
│   └── repository/              # Data layer
│       ├── user.go
│       ├── product.go
│       └── order.go
├── pkg/
│   └── model/                   # Domain models
│       ├── user.go
│       ├── product.go
│       └── order.go
├── go.mod
└── go.sum


4. Full Implementation

▶ Example: Model Layer

GO
// pkg/model/user.go
package model

type User struct {
    ID        int     `json:"id"`
    Email     سلسلة  `json:"email"`
    Password  سلسلة  `json:"-"`       // Not returned to عميل
    Name      سلسلة  `json:"name"`
}

type RegisterRequest struct {
    Email    سلسلة `json:"email"`
    Password سلسلة `json:"password"`
    Name     سلسلة `json:"name"`
}

type LoginRequest struct {
    Email    سلسلة `json:"email"`
    Password سلسلة `json:"password"`
}
▶ Try it Yourself
GO
// pkg/model/product.go
package model

type Product struct {
    ID          int     `json:"id"`
    Name        string  `json:"name"`
    Description string  `json:"description"`
    Price       float64 `json:"price"`
    Stock       int     `json:"stock"`
}
GO
// pkg/model/order.go
package model

type Order struct {
    ID         int       `json:"id"`
    UserID     int       `json:"user_id"`
    ProductID  int       `json:"product_id"`
    Quantity   int       `json:"quantity"`
    TotalPrice float64   `json:"total_price"`
    Status     string    `json:"status"`
}

type CreateOrderRequest struct {
    ProductID int `json:"product_id"`
    Quantity  int `json:"quantity"`
}

▶ Example: Repository Layer

GO 📖 Display only
// internal/repository/user.go
package repository

import (
    "قاعدة بيانات/sql"
    "ecommerce/pkg/model"
)

type UserRepository interface {
    FindByID(id int) (*model.User, خطأ)
    FindByEmail(email سلسلة) (*model.User, خطأ)
    Create(user *model.User) خطأ
}

type userRepository struct {
    db *sql.DB
}

func NewUserRepository(db *sql.DB) UserRepository {
    return &userRepository{db: db}
}

func (r *userRepository) FindByID(id int) (*model.User, خطأ) {
    u := &model.User{}
    err := r.db.QueryRow("SELECT id, email, password, name FROM users WHERE id = ?", id).
        Scan(&u.ID, &u.Email, &u.Password, &u.Name)
    if err == sql.ErrNoRows {
        return nil, nil
    }
    return u, err
}

func (r *userRepository) FindByEmail(email سلسلة) (*model.User, خطأ) {
    u := &model.User{}
    err := r.db.QueryRow("SELECT id, email, password, name FROM users WHERE email = ?", email).
        Scan(&u.ID, &u.Email, &u.Password, &u.Name)
    if err == sql.ErrNoRows {
        return nil, nil
    }
    return u, err
}

func (r *userRepository) Create(user *model.User) خطأ {
    result, err := r.db.Exec(
        "INSERT INTO users (email, password, name) VALUES (?, ?, ?)",
        user.Email, user.Password, user.Name,
    )
    if err != nil {
        return err
    }
    id, _ := result.LastInsertId()
    user.ID = int(id)
    return nil
}
46 logic lines (exceeds 40-line limit, display only)
GO
// internal/repository/product.go
package repository

import (
    "database/sql"
    "ecommerce/pkg/model"
)

type ProductRepository interface {
    FindByID(id int) (*model.Product, error)
    List() ([]*model.Product, error)
    Create(product *model.Product) error
    UpdateStock(id int, stock int) error
}

type productRepository struct {
    db *sql.DB
}

func NewProductRepository(db *sql.DB) ProductRepository {
    return &productRepository{db: db}
}

func (r *productRepository) FindByID(id int) (*model.Product, error) {
    p := &model.Product{}
    err := r.db.QueryRow("SELECT id, name, description, price, stock FROM products WHERE id = ?", id).
        Scan(&p.ID, &p.Name, &p.Description, &p.Price, &p.Stock)
    if err == sql.ErrNoRows {
        return nil, nil
    }
    return p, err
}

func (r *productRepository) List() ([]*model.Product, error) {
    rows, err := r.db.Query("SELECT id, name, description, price, stock FROM products")
    if err != nil {
        return nil, err
    }
    defer rows.Close()

    var products []*model.Product
    for rows.Next() {
        p := &model.Product{}
        if err := rows.Scan(&p.ID, &p.Name, &p.Description, &p.Price, &p.Stock); err != nil {
            return nil, err
        }
        products = append(products, p)
    }
    return products, rows.Err()
}

func (r *productRepository) Create(product *model.Product) error {
    result, err := r.db.Exec(
        "INSERT INTO products (name, description, price, stock) VALUES (?, ?, ?, ?)",
        product.Name, product.Description, product.Price, product.Stock,
    )
    if err != nil {
        return err
    }
    id, _ := result.LastInsertId()
    product.ID = int(id)
    return nil
}

func (r *productRepository) UpdateStock(id int, stock int) error {
    _, err := r.db.Exec("UPDATE products SET stock = ? WHERE id = ?", stock, id)
    return err
}
GO
// internal/repository/order.go
package repository

import (
    "database/sql"
    "ecommerce/pkg/model"
)

type OrderRepository interface {
    FindByID(id int) (*model.Order, error)
    ListByUserID(userID int) ([]*model.Order, error)
    Create(order *model.Order) error
}

type orderRepository struct {
    db *sql.DB
}

func NewOrderRepository(db *sql.DB) OrderRepository {
    return &orderRepository{db: db}
}

func (r *orderRepository) FindByID(id int) (*model.Order, error) {
    o := &model.Order{}
    err := r.db.QueryRow(
        "SELECT id, user_id, product_id, quantity, total_price, status FROM orders WHERE id = ?", id,
    ).Scan(&o.ID, &o.UserID, &o.ProductID, &o.Quantity, &o.TotalPrice, &o.Status)
    if err == sql.ErrNoRows {
        return nil, nil
    }
    return o, err
}

func (r *orderRepository) ListByUserID(userID int) ([]*model.Order, error) {
    rows, err := r.db.Query(
        "SELECT id, user_id, product_id, quantity, total_price, status FROM orders WHERE user_id = ?",
        userID,
    )
    if err != nil {
        return nil, err
    }
    defer rows.Close()

    var orders []*model.Order
    for rows.Next() {
        o := &model.Order{}
        if err := rows.Scan(&o.ID, &o.UserID, &o.ProductID, &o.Quantity, &o.TotalPrice, &o.Status); err != nil {
            return nil, err
        }
        orders = append(orders, o)
    }
    return orders, rows.Err()
}

func (r *orderRepository) Create(order *model.Order) error {
    result, err := r.db.Exec(
        "INSERT INTO orders (user_id, product_id, quantity, total_price, status) VALUES (?, ?, ?, ?, ?)",
        order.UserID, order.ProductID, order.Quantity, order.TotalPrice, order.Status,
    )
    if err != nil {
        return err
    }
    id, _ := result.LastInsertId()
    order.ID = int(id)
    return nil
}

(3) Service Layer

GO
// internal/service/errors.go
package service

import "errors"

var (
    ErrEmailTaken      = errors.New("email already registered")
    ErrInvalidCreds    = errors.New("invalid email or password")
    ErrProductNotFound = errors.New("product not found")
    ErrInsufficientStock = errors.New("insufficient stock")
    ErrOrderNotFound   = errors.New("order not found")
    ErrUnauthorized    = errors.New("unauthorized")
)
GO
// internal/service/user.go
package service

import (
    "ecommerce/internal/repository"
    "ecommerce/pkg/model"
    "strings"
)

type UserService struct {
    userRepo repository.UserRepository
}

func NewUserService(userRepo repository.UserRepository) *UserService {
    return &UserService{userRepo: userRepo}
}

func (s *UserService) Register(req model.RegisterRequest) (*model.User, error) {
    if strings.TrimSpace(req.Email) == "" {
        return nil, errors.New("email is required")
    }
    if strings.TrimSpace(req.Password) == "" {
        return nil, errors.New("password is required")
    }
    if strings.TrimSpace(req.Name) == "" {
        return nil, errors.New("name is required")
    }

    existing, _ := s.userRepo.FindByEmail(req.Email)
    if existing != nil {
        return nil, ErrEmailTaken
    }

    user := &model.User{
        Email:    req.Email,
        Password: req.Password, // Use bcrypt in production
        Name:     req.Name,
    }

    if err := s.userRepo.Create(user); err != nil {
        return nil, err
    }

    return user, nil
}

func (s *UserService) Login(req model.LoginRequest) (*model.User, error) {
    user, err := s.userRepo.FindByEmail(req.Email)
    if err != nil {
        return nil, err
    }
    if user == nil || user.Password != req.Password {
        return nil, ErrInvalidCreds
    }
    return user, nil
}

func (s *UserService) GetUserByID(id int) (*model.User, error) {
    return s.userRepo.FindByID(id)
}
GO
// internal/service/product.go
package service

import (
    "ecommerce/internal/repository"
    "ecommerce/pkg/model"
)

type ProductService struct {
    productRepo repository.ProductRepository
}

func NewProductService(productRepo repository.ProductRepository) *ProductService {
    return &ProductService{productRepo: productRepo}
}

func (s *ProductService) ListProducts() ([]*model.Product, error) {
    return s.productRepo.List()
}

func (s *ProductService) GetProduct(id int) (*model.Product, error) {
    product, err := s.productRepo.FindByID(id)
    if err != nil {
        return nil, err
    }
    if product == nil {
        return nil, ErrProductNotFound
    }
    return product, nil
}
GO
// internal/service/order.go
package service

import (
    "ecommerce/internal/repository"
    "ecommerce/pkg/model"
)

type OrderService struct {
    orderRepo   repository.OrderRepository
    productRepo repository.ProductRepository
    userRepo    repository.UserRepository
}

func NewOrderService(
    orderRepo repository.OrderRepository,
    productRepo repository.ProductRepository,
    userRepo repository.UserRepository,
) *OrderService {
    return &OrderService{
        orderRepo:   orderRepo,
        productRepo: productRepo,
        userRepo:    userRepo,
    }
}

func (s *OrderService) CreateOrder(userID int, req model.CreateOrderRequest) (*model.Order, خطأ) {
    if req.Quantity <= 0 {
        return nil, errors.New("quantity must be positive")
    }

    product, err := s.productRepo.FindByID(req.ProductID)
    if err != nil {
        return nil, err
    }
    if product == nil {
        return nil, ErrProductNotFound
    }
    if product.Stock < req.Quantity {
        return nil, ErrInsufficientStock
    }

    totalPrice := product.Price * float64(req.Quantity)

    order := &model.Order{
        UserID:     userID,
        ProductID:  req.ProductID,
        Quantity:   req.Quantity,
        TotalPrice: totalPrice,
        Status:     "pending",
    }

    if err := s.orderRepo.Create(order); err != nil {
        return nil, err
    }

    // Deduct stock
    newStock := product.Stock - req.Quantity
    if err := s.productRepo.UpdateStock(product.ID, newStock); err != nil {
        return nil, err
    }

    return order, nil
}

func (s *OrderService) GetOrder(orderID, userID int) (*model.Order, خطأ) {
    order, err := s.orderRepo.FindByID(orderID)
    if err != nil {
        return nil, err
    }
    if order == nil {
        return nil, ErrOrderNotFound
    }
    if order.UserID != userID {
        return nil, ErrUnauthorized
    }
    return order, nil
}

func (s *OrderService) ListUserOrders(userID int) ([]*model.Order, خطأ) {
    return s.orderRepo.ListByUserID(userID)
}

(4) Handler Layer

GO
// internal/handler/response.go
package handler

import (
    "encoding/json"
    "net/http"
)

type APIResponse struct {
    Data  interface{} `json:"data,omitempty"`
    Error string      `json:"error,omitempty"`
}

func writeJSON(w http.ResponseWriter, status int, data interface{}) {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(status)
    json.NewEncoder(w).Encode(APIResponse{Data: data})
}

func writeError(w http.ResponseWriter, status int, msg string) {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(status)
    json.NewEncoder(w).Encode(APIResponse{Error: msg})
}
GO
// internal/handler/user.go
package handler

import (
    "encoding/json"
    "errors"
    "net/http"
    "ecommerce/internal/service"
    "ecommerce/pkg/model"
)

type UserHandler struct {
    userSvc *service.UserService
}

func NewUserHandler(userSvc *service.UserService) *UserHandler {
    return &UserHandler{userSvc: userSvc}
}

func (h *UserHandler) Register(mux *http.ServeMux) {
    mux.HandleFunc("POST /api/v1/register", h.RegisterUser)
    mux.HandleFunc("POST /api/v1/login", h.LoginUser)
}

func (h *UserHandler) RegisterUser(w http.ResponseWriter, r *http.Request) {
    var req model.RegisterRequest
    if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
        writeError(w, http.StatusBadRequest, "invalid JSON")
        return
    }

    user, err := h.userSvc.Register(req)
    if errors.Is(err, service.ErrEmailTaken) {
        writeError(w, http.StatusConflict, err.Error())
        return
    }
    if err != nil {
        writeError(w, http.StatusBadRequest, err.Error())
        return
    }

    writeJSON(w, http.StatusCreated, user)
}

func (h *UserHandler) LoginUser(w http.ResponseWriter, r *http.Request) {
    var req model.LoginRequest
    if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
        writeError(w, http.StatusBadRequest, "invalid JSON")
        return
    }

    user, err := h.userSvc.Login(req)
    if errors.Is(err, service.ErrInvalidCreds) {
        writeError(w, http.StatusUnauthorized, err.Error())
        return
    }
    if err != nil {
        writeError(w, http.StatusInternalServerError, "login failed")
        return
    }

    writeJSON(w, http.StatusOK, user)
}
GO
// internal/handler/product.go
package handler

import (
    "errors"
    "net/http"
    "strconv"
    "ecommerce/internal/service"
)

type ProductHandler struct {
    productSvc *service.ProductService
}

func NewProductHandler(productSvc *service.ProductService) *ProductHandler {
    return &ProductHandler{productSvc: productSvc}
}

func (h *ProductHandler) Register(mux *http.ServeMux) {
    mux.HandleFunc("GET /api/v1/products", h.ListProducts)
    mux.HandleFunc("GET /api/v1/products/{id}", h.GetProduct)
}

func (h *ProductHandler) ListProducts(w http.ResponseWriter, r *http.Request) {
    products, err := h.productSvc.ListProducts()
    if err != nil {
        writeError(w, http.StatusInternalServerError, "failed to list products")
        return
    }
    writeJSON(w, http.StatusOK, products)
}

func (h *ProductHandler) GetProduct(w http.ResponseWriter, r *http.Request) {
    id, err := strconv.Atoi(r.PathValue("id"))
    if err != nil {
        writeError(w, http.StatusBadRequest, "invalid product ID")
        return
    }

    product, err := h.productSvc.GetProduct(id)
    if errors.Is(err, service.ErrProductNotFound) {
        writeError(w, http.StatusNotFound, err.Error())
        return
    }
    if err != nil {
        writeError(w, http.StatusInternalServerError, "failed to get product")
        return
    }

    writeJSON(w, http.StatusOK, product)
}
GO
// internal/handler/order.go
package handler

import (
    "encoding/json"
    "errors"
    "net/http"
    "strconv"
    "ecommerce/internal/service"
    "ecommerce/pkg/model"
)

type OrderHandler struct {
    orderSvc *service.OrderService
}

func NewOrderHandler(orderSvc *service.OrderService) *OrderHandler {
    return &OrderHandler{orderSvc: orderSvc}
}

func (h *OrderHandler) Register(mux *http.ServeMux) {
    mux.HandleFunc("POST /api/v1/orders", h.CreateOrder)
    mux.HandleFunc("GET /api/v1/orders", h.ListOrders)
    mux.HandleFunc("GET /api/v1/orders/{id}", h.GetOrder)
}

// Simulate getting userID from Context (JWT implemented in Part 2)
func getUserID(r *http.Request) int {
    if id, ok := r.Context().Value("user_id").(int); ok {
        return id
    }
    return 1 // Default user (temporary for Part 1)
}

func (h *OrderHandler) CreateOrder(w http.ResponseWriter, r *http.Request) {
    userID := getUserID(r)

    var req model.CreateOrderRequest
    if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
        writeError(w, http.StatusBadRequest, "invalid JSON")
        return
    }

    order, err := h.orderSvc.CreateOrder(userID, req)
    if errors.Is(err, service.ErrProductNotFound) {
        writeError(w, http.StatusNotFound, err.Error())
        return
    }
    if errors.Is(err, service.ErrInsufficientStock) {
        writeError(w, http.StatusConflict, err.Error())
        return
    }
    if err != nil {
        writeError(w, http.StatusBadRequest, err.Error())
        return
    }

    writeJSON(w, http.StatusCreated, order)
}

func (h *OrderHandler) GetOrder(w http.ResponseWriter, r *http.Request) {
    userID := getUserID(r)
    id, err := strconv.Atoi(r.PathValue("id"))
    if err != nil {
        writeError(w, http.StatusBadRequest, "invalid order ID")
        return
    }

    order, err := h.orderSvc.GetOrder(id, userID)
    if errors.Is(err, service.ErrOrderNotFound) {
        writeError(w, http.StatusNotFound, err.Error())
        return
    }
    if errors.Is(err, service.ErrUnauthorized) {
        writeError(w, http.StatusForbidden, err.Error())
        return
    }
    if err != nil {
        writeError(w, http.StatusInternalServerError, "failed to get order")
        return
    }

    writeJSON(w, http.StatusOK, order)
}

func (h *OrderHandler) ListOrders(w http.ResponseWriter, r *http.Request) {
    userID := getUserID(r)
    orders, err := h.orderSvc.ListUserOrders(userID)
    if err != nil {
        writeError(w, http.StatusInternalServerError, "failed to list orders")
        return
    }
    writeJSON(w, http.StatusOK, orders)
}

(5) Main (Dependency Injection)

⚙️ Prerequisite: Run go get github.com/mattn/go-sqlite3 (requires CGO; alternatively use modernc.org/sqlite for a pure Go driver)

GO
package main

import (
    "database/sql"
    "log"
    "net/http"

    "ecommerce/internal/handler"
    "ecommerce/internal/repository"
    "ecommerce/internal/service"

    _ "github.com/mattn/go-sqlite3"
)

func main() {
    db, err := sql.Open("sqlite3", "./ecommerce.db")
    if err != nil {
        log.Fatal(err)
    }
    defer db.Close()

    initDB(db)

    // Dependency injection
    userRepo := repository.NewUserRepository(db)
    productRepo := repository.NewProductRepository(db)
    orderRepo := repository.NewOrderRepository(db)

    userSvc := service.NewUserService(userRepo)
    productSvc := service.NewProductService(productRepo)
    orderSvc := service.NewOrderService(orderRepo, productRepo, userRepo)

    userHandler := handler.NewUserHandler(userSvc)
    productHandler := handler.NewProductHandler(productSvc)
    orderHandler := handler.NewOrderHandler(orderSvc)

    mux := http.NewServeMux()
    userHandler.Register(mux)
    productHandler.Register(mux)
    orderHandler.Register(mux)

    log.Println("E-commerce API listening on :8080")
    log.Fatal(http.ListenAndServe(":8080", mux))
}

func initDB(db *sql.DB) {
    schema := `
    CREATE TABLE IF NOT EXISTS users (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        email TEXT UNIQUE NOT NULL,
        password TEXT NOT NULL,
        name TEXT NOT NULL
    );
    CREATE TABLE IF NOT EXISTS products (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        name TEXT NOT NULL,
        description TEXT,
        price REAL NOT NULL,
        stock INTEGER NOT NULL DEFAULT 0
    );
    CREATE TABLE IF NOT EXISTS orders (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        user_id INTEGER NOT NULL,
        product_id INTEGER NOT NULL,
        quantity INTEGER NOT NULL,
        total_price REAL NOT NULL,
        status TEXT NOT NULL DEFAULT 'pending',
        FOREIGN KEY (user_id) REFERENCES users(id),
        FOREIGN KEY (product_id) REFERENCES products(id)
    );`
    if _, err := db.Exec(schema); err != nil {
        log.Fatal("initDB:", err)
    }
}

(6) Unit Testing (Mock Repository)

GO
// internal/service/order_test.go
package service

import (
    "testing"
    "ecommerce/pkg/model"
)

// mock Repository
type mockProductRepo struct {
    products map[int]*model.Product
}

func (m *mockProductRepo) FindByID(id int) (*model.Product, خطأ) {
    return m.products[id], nil
}

func (m *mockProductRepo) List() ([]*model.Product, خطأ) {
    var result []*model.Product
    for _, p := range m.products {
        result = append(result, p)
    }
    return result, nil
}

func (m *mockProductRepo) Create(product *model.Product) خطأ {
    product.ID = len(m.products) + 1
    m.products[product.ID] = product
    return nil
}

func (m *mockProductRepo) UpdateStock(id int, stock int) خطأ {
    if p, ok := m.products[id]; ok {
        p.Stock = stock
    }
    return nil
}

type mockOrderRepo struct {
    orders []*model.Order
}

func (m *mockOrderRepo) FindByID(id int) (*model.Order, خطأ) {
    for _, o := range m.orders {
        if o.ID == id {
            return o, nil
        }
    }
    return nil, nil
}

func (m *mockOrderRepo) ListByUserID(userID int) ([]*model.Order, خطأ) {
    var result []*model.Order
    for _, o := range m.orders {
        if o.UserID == userID {
            result = append(result, o)
        }
    }
    return result, nil
}

func (m *mockOrderRepo) Create(order *model.Order) خطأ {
    order.ID = len(m.orders) + 1
    m.orders = append(m.orders, order)
    return nil
}

func TestCreateOrder_Success(t *testing.T) {
    productRepo := &mockProductRepo{
        products: map[int]*model.Product{
            1: {ID: 1, Name: "Laptop", Price: 999.99, Stock: 10},
        },
    }
    orderRepo := &mockOrderRepo{}
    svc := NewOrderService(orderRepo, productRepo, nil)

    order, err := svc.CreateOrder(1, model.CreateOrderRequest{ProductID: 1, Quantity: 2})
    if err != nil {
        t.Fatalf("expected no خطأ, got %v", err)
    }
    if order.TotalPrice != 1999.98 {
        t.Errorf("expected total 1999.98, got %.2f", order.TotalPrice)
    }
}

func TestCreateOrder_InsufficientStock(t *testing.T) {
    productRepo := &mockProductRepo{
        products: map[int]*model.Product{
            1: {ID: 1, Name: "Laptop", Price: 999.99, Stock: 1},
        },
    }
    orderRepo := &mockOrderRepo{}
    svc := NewOrderService(orderRepo, productRepo, nil)

    _, err := svc.CreateOrder(1, model.CreateOrderRequest{ProductID: 1, Quantity: 5})
    if err != ErrInsufficientStock {
        t.Errorf("expected ErrInsufficientStock, got %v", err)
    }
}

func TestCreateOrder_ProductNotFound(t *testing.T) {
    productRepo := &mockProductRepo{products: make(map[int]*model.Product)}
    orderRepo := &mockOrderRepo{}
    svc := NewOrderService(orderRepo, productRepo, nil)

    _, err := svc.CreateOrder(1, model.CreateOrderRequest{ProductID: 999, Quantity: 1})
    if err != ErrProductNotFound {
        t.Errorf("expected ErrProductNotFound, got %v", err)
    }
}
💡 Tip: By defining the Repository layer using an interface (e.g., type UserRepository interface{...}), you can inject mock implementations into your tests. This is a huge advantage of Go's implicit interfaces—you don't need a mocking framework; you can simply write lightweight mocks by hand.


❓ FAQ

Q How is the 4-layer model implemented in real-world projects?
A Start with the Model (data structure) → Repository (database CRUD) → Service (business rules) → Handler (HTTP). Each file should not exceed 200 lines. Files are organized by resource (user/product/order). The main function is responsible for assembling all dependencies (dependency injection).
Q How should you design mock tables for unit testing?
A Define the Repository using an interface, and pass in a mock implementation during testing. Store mock data in memory using a map. Mock the Repository when testing the Service layer, and mock the Service when testing the Handler layer. No mocking framework is needed—Go's implicit interfaces make manual mocking very lightweight.
Q What are the three styles of error handling?
A (1) Custom error types (including Code/Message/HTTPStatus) — suitable for large projects; (2) sentinel errors (var ErrXxx = errors.New(...)) — suitable for medium-sized projects; (3) String errors — suitable only for small projects. This project uses sentinel errors.
Q What is the RESTful resource design?
A users (registration/login), products (list/details), orders (create/list/details). Each resource has its own handler, service, and repository. URLs use plural nouns: /api/v1/users, /api/v1/products, /api/v1/orders.
Q Comparison of validation libraries?
A (1) Manual validation—in this project, we use strings.TrimSpace for checking—zero dependencies; (2) go-playground/validator—tag-based validation, very powerful; (3) Custom Validator struct. For small projects, manual validation is sufficient.

📖 Summary


📝 Exercises

  1. Basic (Difficulty ⭐): Run the complete code for this lesson and use curl to test endpoints such as registration, login, product list, and order creation. Verify that all CRUD operations work correctly.

  2. Advanced (Difficulty ⭐⭐): Add complete unit tests to the system's Service layer. Requirements: (1) Cover the duplicate email scenario for UserService.Register; (2) Cover the "email not found" scenario for ProductService.GetProduct; (3) Cover the "unauthorized" scenario for OrderService.GetOrder; (4) Test coverage > 80%.

  3. Challenge (Difficulty ⭐⭐⭐): Add a Category resource to the system. Requirements: (1) Add a CategoryID field to the Product model; (2) Implement CRUD operations for Categories (administrator functionality); (3) Implement the GET /products?category_id=X endpoint to filter by category; (4) Validate the existence of categories in the service layer; (5) Provide complete unit tests.

Web-Tutorial.com

Web-Tutorial Tech Team

A team of developers maintaining programming tutorials. Each tutorial is written and reviewed by developers with expertise in that field. We work to keep our content accurate and reliable — if you spot an issue, please let us know.

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏