backend-service-patterns

linehaul-ai's avatarfrom linehaul-ai

Use when implementing API endpoints, business logic, database operations, or adding new entities to laneweaverTMS Go/Echo backend.

⭐1stars🔀0forks📁View on GitHub🕐Updated Dec 31, 2025

When & Why to Use This Skill

This Claude skill provides a comprehensive set of standardized backend development patterns for the laneweaverTMS project, utilizing Go, the Echo framework, and PostgreSQL. It streamlines the implementation of API endpoints, business logic, and database operations by enforcing a clean, layered architecture (Handler-Service-Repository) and providing reusable templates for validation, transactions, and OpenAPI documentation.

Use Cases

  • Implementing new RESTful API endpoints with standardized request binding, validation, and error handling using the Echo framework.
  • Developing complex business services that coordinate multiple repositories and handle domain-specific logic with clear separation of concerns.
  • Creating robust data access layers with PostgreSQL using pgx, including safe transaction management and dynamic query building for advanced filtering.
  • Defining consistent data models and DTOs with appropriate JSON and DB tags to ensure seamless API serialization and database persistence.
  • Generating and maintaining up-to-date API documentation using OpenAPI 3.0/Swagger annotations directly within the Go source code.
namebackend-service-patterns
descriptionUse when implementing API endpoints, business logic, database operations, or adding new entities to laneweaverTMS Go/Echo backend.

Backend Service Patterns - Go/Echo/pgx for laneweaverTMS

When to Use This Skill

  • Implementing new API endpoints (handlers)
  • Adding business logic (services)
  • Writing database queries (repositories)
  • Creating models and DTOs
  • Handling transactions
  • Building complex filters with QueryBuilder
  • Adding validation logic
  • Documenting APIs with OpenAPI 3.0 / Swagger annotations

Layered Architecture

HTTP Request → Handler → Service → Repository → PostgreSQL
                 ↓           ↓           ↓
             Bind/JSON   Business    SQL/pgx
             Validate     Logic    Transaction
                 ↓           ↓           ↓
HTTP Response ← Handler ← Service ← Repository

Layer Responsibilities:

  • Handler (/internal/handlers/): HTTP request/response, Echo context, bind JSON, return APIResponse
  • Service (/internal/services/): Business logic, validation, orchestration, coordinate repos
  • Repository (/internal/repository/): SQL queries, transactions, pgx operations
  • Models (/internal/models/): Structs with db/json tags, enums, request/response DTOs

Handler Patterns

Handler Struct with Dependency Injection

type LoadHandler struct {
    service *services.LoadService
}

func NewLoadHandler(service *services.LoadService) *LoadHandler {
    return &LoadHandler{service: service}
}

Create Handler (POST)

func (h *LoadHandler) Create(c echo.Context) error {
    var req models.CreateLoadRequest
    if err := c.Bind(&req); err != nil {
        return c.JSON(http.StatusBadRequest, models.APIResponse{
            Success: false,
            Message: "Invalid request body",
            Errors:  map[string][]string{"body": {"Failed to parse JSON"}},
        })
    }

    result, err := h.service.CreateLoad(c.Request().Context(), &req)
    if err != nil {
        // Validation errors → 400
        if validationErrors, ok := err.(models.ValidationErrors); ok {
            return c.JSON(http.StatusBadRequest, models.APIResponse{
                Success: false,
                Message: "Validation failed",
                Errors:  validationErrors.ToMap(),
            })
        }
        // Operational errors → 500
        c.Logger().Error("CreateLoad error:", err)
        return c.JSON(http.StatusInternalServerError, models.APIResponse{
            Success: false,
            Message: "Failed to create load",
        })
    }

    return c.JSON(http.StatusCreated, models.APIResponse{
        Success: true,
        Message: "Load created successfully",
        Data:    result,
    })
}

List Handler with Filters (GET)

func (h *LoadHandler) List(c echo.Context) error {
    filters := parseLoadFilters(c)

    items, total, err := h.service.ListLoads(c.Request().Context(), filters)
    if err != nil {
        return c.JSON(http.StatusInternalServerError, models.APIResponse{
            Success: false,
            Message: "Failed to fetch loads",
        })
    }

    response := models.NewPaginatedResponse(items, total, filters.Page, filters.PageSize)
    return c.JSON(http.StatusOK, models.APIResponse{
        Success: true,
        Data:    response,
    })
}

Query Parameter Parsing (Nil Pointer Helpers)

func parseLoadFilters(c echo.Context) models.LoadFilters {
    var filters models.LoadFilters

    // Multi-select (comma-separated)
    if loadStatus := c.QueryParam("load_status"); loadStatus != "" {
        filters.LoadStatus = strings.Split(loadStatus, ",")
    }

    // Optional string
    filters.AccountID = parseStringPtr(c.QueryParam("account_id"))

    // Date range
    filters.PickupDateFrom = parseDatePtr(c.QueryParam("pickup_date_from"))
    filters.PickupDateTo = parseDatePtr(c.QueryParam("pickup_date_to"))

    // Pagination with defaults
    filters.Page = 1
    if page := c.QueryParam("page"); page != "" {
        if p, err := strconv.Atoi(page); err == nil && p > 0 {
            filters.Page = p
        }
    }
    filters.PageSize = 50
    if pageSize := c.QueryParam("page_size"); pageSize != "" {
        if ps, err := strconv.Atoi(pageSize); err == nil && ps > 0 && ps <= 100 {
            filters.PageSize = ps
        }
    }

    return filters
}

// Helper functions for safe type conversion
func parseStringPtr(s string) *string {
    if s == "" {
        return nil
    }
    return &s
}

func parseDatePtr(s string) *time.Time {
    if s == "" {
        return nil
    }
    t, err := time.Parse("2006-01-02", s)
    if err != nil {
        return nil
    }
    return &t
}

func parseFloatPtr(s string) *float64 {
    if s == "" {
        return nil
    }
    f, err := strconv.ParseFloat(s, 64)
    if err != nil {
        return nil
    }
    return &f
}

func parseBoolPtr(s string) *bool {
    if s == "" {
        return nil
    }
    b, err := strconv.ParseBool(s)
    if err != nil {
        return nil
    }
    return &b
}

GetByID Handler

func (h *LoadHandler) GetByID(c echo.Context) error {
    id := c.Param("id")

    load, err := h.service.GetLoadByID(c.Request().Context(), id)
    if err != nil {
        return c.JSON(http.StatusInternalServerError, models.APIResponse{
            Success: false,
            Message: "Failed to fetch load",
        })
    }
    if load == nil {
        return c.JSON(http.StatusNotFound, models.APIResponse{
            Success: false,
            Message: "Load not found",
        })
    }

    return c.JSON(http.StatusOK, models.APIResponse{
        Success: true,
        Data:    load,
    })
}

Service Patterns

Service Struct with Multiple Repositories

type LoadService struct {
    loadRepo     *repository.LoadRepository
    facilityRepo *repository.FacilityRepository
    accountRepo  *repository.AccountRepository
}

func NewLoadService(
    loadRepo *repository.LoadRepository,
    facilityRepo *repository.FacilityRepository,
    accountRepo *repository.AccountRepository,
) *LoadService {
    return &LoadService{
        loadRepo:     loadRepo,
        facilityRepo: facilityRepo,
        accountRepo:  accountRepo,
    }
}

Create Method with Validation

func (s *LoadService) CreateLoad(ctx context.Context, req *models.CreateLoadRequest) (*models.CreateLoadResponse, error) {
    // 1. Validate request (delegated to models)
    if errors := models.ValidateCreateLoadRequest(req); len(errors) > 0 {
        return nil, errors
    }

    // 2. Business logic (find-or-create facilities, generate IDs, etc.)
    stopParams, err := s.resolveStopFacilities(ctx, req.Stops)
    if err != nil {
        return nil, err
    }

    // 3. Build entity
    load := &models.Load{
        ID:           uuid.New().String(),
        LoadNumber:   s.generateLoadNumber(ctx),
        TenderID:     req.TenderID,
        LoadStatus:   models.LoadStatusUncovered,
        Mode:         &req.Mode,
        CustomerRate: &req.CustomerRate,
    }

    // 4. Call repository
    createParams := repository.CreateLoadParams{Load: load, Stops: stopParams}
    if err := s.loadRepo.CreateWithStops(ctx, createParams); err != nil {
        return nil, fmt.Errorf("failed to create load: %w", err)
    }

    // 5. Build response
    return &models.CreateLoadResponse{
        ID:         load.ID,
        LoadNumber: load.LoadNumber,
        LoadStatus: load.LoadStatus,
    }, nil
}

Retry Logic for Race Conditions

const maxRetries = 3

func (s *LoadService) CreateLoadWithRetry(ctx context.Context, req *models.CreateLoadRequest) (*models.Load, error) {
    var lastErr error
    for attempt := 0; attempt < maxRetries; attempt++ {
        loadNumber, err := s.generateLoadNumber(ctx)
        if err != nil {
            return nil, err
        }

        load := &models.Load{
            ID:         uuid.New().String(),
            LoadNumber: loadNumber,
            // ...
        }

        err = s.loadRepo.Create(ctx, load)
        if err != nil {
            // Retry on unique constraint violation
            if strings.Contains(err.Error(), "duplicate key") ||
                strings.Contains(err.Error(), "loads_load_number_key") {
                lastErr = err
                continue
            }
            return nil, err
        }
        return load, nil
    }
    return nil, fmt.Errorf("failed after %d retries: %w", maxRetries, lastErr)
}

Repository Patterns

Repository Struct

type LoadRepository struct {
    db *database.Client
}

func NewLoadRepository(db *database.Client) *LoadRepository {
    return &LoadRepository{db: db}
}

Transaction Pattern

func (r *LoadRepository) CreateWithStops(ctx context.Context, params CreateLoadParams) error {
    tx, err := r.db.Begin(ctx)
    if err != nil {
        return fmt.Errorf("failed to begin transaction: %w", err)
    }
    defer tx.Rollback(ctx)

    // Insert load
    loadQuery := `
        INSERT INTO loads (id, load_number, tender_id, load_status, mode, customer_rate, created_at, updated_at)
        VALUES ($1, $2, $3, $4, $5, $6, NOW(), NOW())
    `
    _, err = tx.Exec(ctx, loadQuery,
        params.Load.ID,
        params.Load.LoadNumber,
        params.Load.TenderID,
        params.Load.LoadStatus,
        params.Load.Mode,
        params.Load.CustomerRate,
    )
    if err != nil {
        return fmt.Errorf("failed to insert load: %w", err)
    }

    // Insert stops
    stopQuery := `
        INSERT INTO stops (id, load_id, facility_id, stop_sequence, stop_type, scheduled_date, created_at, updated_at)
        VALUES ($1, $2, $3, $4, $5, $6, NOW(), NOW())
    `
    for _, stop := range params.Stops {
        _, err = tx.Exec(ctx, stopQuery,
            stop.ID, params.Load.ID, stop.FacilityID, stop.StopSequence, stop.StopType, stop.ScheduledDate,
        )
        if err != nil {
            return fmt.Errorf("failed to insert stop %d: %w", stop.StopSequence, err)
        }
    }

    if err = tx.Commit(ctx); err != nil {
        return fmt.Errorf("failed to commit: %w", err)
    }
    return nil
}

GetByID with Not Found Handling

func (r *LoadRepository) GetByID(ctx context.Context, id string) (*models.Load, error) {
    query := `
        SELECT id, load_number, tender_id, load_status, mode, customer_rate, created_at, updated_at
        FROM loads
        WHERE id = $1 AND deleted_at IS NULL
    `
    var load models.Load
    err := r.db.QueryRow(ctx, query, id).Scan(
        &load.ID, &load.LoadNumber, &load.TenderID, &load.LoadStatus,
        &load.Mode, &load.CustomerRate, &load.CreatedAt, &load.UpdatedAt,
    )
    if err != nil {
        if errors.Is(err, pgx.ErrNoRows) {
            return nil, nil // Not found
        }
        return nil, err
    }
    return &load, nil
}

QueryBuilder for Complex Filters

func buildLoadListQuery(filters models.LoadFilters) (string, []interface{}) {
    qb := NewQueryBuilder()

    // Multi-select IN conditions
    qb.AddInCondition("l.load_status", filters.LoadStatus)
    qb.AddInCondition("l.mode", filters.Mode)

    // Single value conditions
    if filters.AccountID != nil {
        qb.AddCondition("t.account_id = $%d", *filters.AccountID)
    }
    if filters.CarrierID != nil {
        qb.AddCondition("l.carrier_id = $%d", *filters.CarrierID)
    }

    // Date ranges
    if filters.PickupDateFrom != nil {
        qb.AddCondition("t.pickup_date >= $%d", *filters.PickupDateFrom)
    }
    if filters.PickupDateTo != nil {
        qb.AddCondition("t.pickup_date <= $%d", *filters.PickupDateTo)
    }

    // Boolean filters
    if filters.IsCancelled != nil {
        qb.AddCondition("l.is_cancelled = $%d", *filters.IsCancelled)
    }

    // Text search
    if filters.Search != nil && *filters.Search != "" {
        qb.AddOrCondition([]string{"l.load_number", "a.name"}, "%"+*filters.Search+"%")
    }

    whereClause, args := qb.Build()

    // Sort column whitelist
    sortColumn := "l.created_at"
    sortColumnMap := map[string]string{
        "load_number": "l.load_number",
        "created_at":  "l.created_at",
        "pickup_date": "t.pickup_date",
    }
    if col, ok := sortColumnMap[filters.SortBy]; ok {
        sortColumn = col
    }

    sortDir := "DESC"
    if filters.SortDir == "asc" {
        sortDir = "ASC"
    }

    offset := (filters.Page - 1) * filters.PageSize
    argIdx := qb.ArgCount()

    baseQuery := `
        SELECT l.id, l.load_number, l.load_status, l.customer_rate, a.name as account_name
        FROM loads l
        JOIN tenders t ON l.tender_id = t.id
        LEFT JOIN accounts a ON t.account_id = a.id
        WHERE l.deleted_at IS NULL
    ` + whereClause

    query := fmt.Sprintf("%s ORDER BY %s %s LIMIT $%d OFFSET $%d",
        baseQuery, sortColumn, sortDir, argIdx+1, argIdx+2)
    args = append(args, filters.PageSize, offset)

    return query, args
}

Find-or-Create Pattern

func (r *FacilityRepository) FindOrCreate(ctx context.Context, addr *models.FacilityAddress) (*models.Facility, error) {
    // Try to find existing
    existing, err := r.FindByAddress(ctx, addr.Address, addr.City, addr.State, addr.Zip)
    if err != nil {
        return nil, err
    }
    if existing != nil {
        return existing, nil
    }

    // Create new
    facility := &models.Facility{
        ID:      uuid.New().String(),
        Name:    fmt.Sprintf("%s, %s", addr.City, addr.State),
        Address: addr.Address,
        City:    addr.City,
        State:   strings.ToUpper(addr.State),
        Zip:     addr.Zip,
        Country: "USA",
    }
    if err := r.Create(ctx, facility); err != nil {
        return nil, err
    }
    return facility, nil
}

Model Patterns

Entity Struct with Tags

type Load struct {
    ID           string          `db:"id" json:"id"`
    LoadNumber   string          `db:"load_number" json:"loadNumber"`
    TenderID     string          `db:"tender_id" json:"tenderId"`
    CarrierID    *string         `db:"carrier_id" json:"carrierId,omitempty"`
    LoadStatus   LoadStatus      `db:"load_status" json:"loadStatus"`
    Mode         *ModeOfTransport `db:"mode" json:"mode,omitempty"`
    CustomerRate *float64        `db:"customer_rate" json:"customerRate,omitempty"`
    CarrierRate  *float64        `db:"carrier_rate" json:"carrierRate,omitempty"`
    IsCancelled  bool            `db:"is_cancelled" json:"isCancelled"`

    // Audit fields
    CreatedAt time.Time  `db:"created_at" json:"createdAt"`
    UpdatedAt time.Time  `db:"updated_at" json:"updatedAt"`
    CreatedBy *int32     `db:"created_by" json:"createdBy,omitempty"`
    UpdatedBy *int32     `db:"updated_by" json:"updatedBy,omitempty"`
    DeletedAt *time.Time `db:"deleted_at" json:"deletedAt,omitempty"`
}

Tag Conventions:

  • db:"column_name" - Maps to PostgreSQL column (snake_case)
  • json:"fieldName" - Maps to JSON field (camelCase for API)
  • json:"...,omitempty" - Omits null/zero values from response
  • Required fields: string, float64, time.Time
  • Optional fields: *string, *float64, *time.Time

Enum Definition with Validation

type LoadStatus string

const (
    LoadStatusUncovered     LoadStatus = "uncovered"
    LoadStatusAssigned      LoadStatus = "assigned"
    LoadStatusDispatched    LoadStatus = "dispatched"
    LoadStatusAtOrigin      LoadStatus = "at_origin"
    LoadStatusInTransit     LoadStatus = "in_transit"
    LoadStatusAtDestination LoadStatus = "at_destination"
    LoadStatusDelivered     LoadStatus = "delivered"
)

func (s LoadStatus) IsValid() bool {
    switch s {
    case LoadStatusUncovered, LoadStatusAssigned, LoadStatusDispatched,
         LoadStatusAtOrigin, LoadStatusInTransit, LoadStatusAtDestination, LoadStatusDelivered:
        return true
    }
    return false
}

type ModeOfTransport string

const (
    ModeDryVan          ModeOfTransport = "Dry Van"
    ModeRefrigeratedVan ModeOfTransport = "Refrigerated Van"
    ModeFlatbed         ModeOfTransport = "Flatbed"
)

func (m ModeOfTransport) IsTempControlled() bool {
    return m == ModeRefrigeratedVan
}

Request DTO

type CreateLoadRequest struct {
    TenderID     string          `json:"tenderId" validate:"required"`
    Mode         ModeOfTransport `json:"mode" validate:"required"`
    CustomerRate float64         `json:"customerRate" validate:"required,gt=0"`
    CarrierRate  *float64        `json:"carrierRate,omitempty" validate:"omitempty,gt=0"`
    Temperature  *float64        `json:"temperature,omitempty"`
    Stops        []CreateStopRequest `json:"stops" validate:"required,min=2"`
}

Validation Function

func ValidateCreateLoadRequest(req *CreateLoadRequest) ValidationErrors {
    var errors ValidationErrors

    if req.TenderID == "" {
        errors = append(errors, ValidationError{
            Field:   "tenderId",
            Message: "Tender ID is required",
        })
    }

    if !req.Mode.IsValid() {
        errors = append(errors, ValidationError{
            Field:   "mode",
            Message: "Invalid mode of transport",
        })
    }

    // Conditional validation
    if req.Mode.IsTempControlled() && req.Temperature == nil {
        errors = append(errors, ValidationError{
            Field:   "temperature",
            Message: fmt.Sprintf("Temperature is required for %s mode", req.Mode),
        })
    }

    if len(req.Stops) < 2 {
        errors = append(errors, ValidationError{
            Field:   "stops",
            Message: "At least 2 stops are required (pickup and delivery)",
        })
    }

    return errors
}

Response & Error Patterns

APIResponse Wrapper

type APIResponse struct {
    Success bool                `json:"success"`
    Message string              `json:"message,omitempty"`
    Data    interface{}         `json:"data,omitempty"`
    Errors  map[string][]string `json:"errors,omitempty"`
}

PaginatedResponse Generic

type PaginatedResponse[T any] struct {
    Data       []T `json:"data"`
    Total      int `json:"total"`
    Page       int `json:"page"`
    PageSize   int `json:"pageSize"`
    TotalPages int `json:"totalPages"`
}

func NewPaginatedResponse[T any](items []T, total, page, pageSize int) PaginatedResponse[T] {
    totalPages := 0
    if total > 0 && pageSize > 0 {
        totalPages = (total + pageSize - 1) / pageSize
    }
    return PaginatedResponse[T]{
        Data:       items,
        Total:      total,
        Page:       page,
        PageSize:   pageSize,
        TotalPages: totalPages,
    }
}

ValidationErrors Type

type ValidationError struct {
    Field   string `json:"field"`
    Message string `json:"message"`
}

type ValidationErrors []ValidationError

func (v ValidationErrors) Error() string {
    if len(v) == 0 {
        return ""
    }
    return fmt.Sprintf("%d validation error(s)", len(v))
}

func (v ValidationErrors) ToMap() map[string][]string {
    result := make(map[string][]string)
    for _, err := range v {
        result[err.Field] = append(result[err.Field], err.Message)
    }
    return result
}

Router Setup

func Setup(e *echo.Echo, loadHandler *handlers.LoadHandler, accountHandler *handlers.AccountHandler) {
    // Global middleware
    e.Use(middleware.SetupLogger())
    e.Use(middleware.SetupRecover())
    e.Use(middleware.SetupCORS())

    // Health check
    e.GET("/health", func(c echo.Context) error {
        return c.JSON(200, map[string]string{"status": "ok"})
    })

    // API v1 routes
    api := e.Group("/api/v1")

    // Loads
    loads := api.Group("/loads")
    loads.GET("", loadHandler.List)
    loads.POST("", loadHandler.Create)
    loads.GET("/:id", loadHandler.GetByID)
    loads.PUT("/:id", loadHandler.Update)
    loads.DELETE("/:id", loadHandler.Delete)

    // Accounts
    accounts := api.Group("/accounts")
    accounts.GET("", accountHandler.List)
    accounts.GET("/:id", accountHandler.GetByID)
}

OpenAPI 3.0 Documentation (swaggo/echo-swagger)

Installation & Setup

Install swag CLI and echo-swagger:

go install github.com/swaggo/swag/cmd/swag@latest
go get github.com/swaggo/echo-swagger

Generate docs (run from project root):

swag init -g cmd/server/main.go -o docs

Register swagger middleware in router:

import (
    echoSwagger "github.com/swaggo/echo-swagger"
    _ "yourproject/docs" // Generated docs
)

func Setup(e *echo.Echo, ...) {
    // Swagger docs endpoint
    e.GET("/swagger/*", echoSwagger.WrapHandler)

    // ... rest of routes
}

Main Package Annotations

Add to cmd/server/main.go:

// @title           laneweaverTMS API
// @version         1.0
// @description     Transportation Management System API

// @host      localhost:8080
// @BasePath  /api/v1

func main() {
    // ...
}

Handler Annotations

Create Handler (POST)

// Create godoc
// @Summary      Create a new load
// @Description  Creates a load with stops and validates required fields
// @Tags         loads
// @Accept       json
// @Produce      json
// @Param        request body models.CreateLoadRequest true "Load creation request"
// @Success      201 {object} models.APIResponse{data=models.CreateLoadResponse}
// @Failure      400 {object} models.APIResponse
// @Failure      500 {object} models.APIResponse
// @Router       /loads [post]
func (h *LoadHandler) Create(c echo.Context) error {
    // ... existing implementation
}

List Handler (GET with Query Params)

// List godoc
// @Summary      List loads with filters
// @Description  Returns paginated list of loads with optional filtering
// @Tags         loads
// @Accept       json
// @Produce      json
// @Param        load_status query string false "Filter by status (comma-separated)" Example(uncovered,assigned)
// @Param        account_id query string false "Filter by account ID"
// @Param        pickup_date_from query string false "Filter pickup date from (YYYY-MM-DD)"
// @Param        pickup_date_to query string false "Filter pickup date to (YYYY-MM-DD)"
// @Param        page query int false "Page number" default(1)
// @Param        page_size query int false "Items per page" default(50) maximum(100)
// @Param        sort_by query string false "Sort column" Enums(load_number,created_at,pickup_date)
// @Param        sort_dir query string false "Sort direction" Enums(asc,desc)
// @Success      200 {object} models.APIResponse{data=models.PaginatedResponse[models.LoadListItem]}
// @Failure      500 {object} models.APIResponse
// @Router       /loads [get]
func (h *LoadHandler) List(c echo.Context) error {
    // ... existing implementation
}

GetByID Handler (GET with Path Param)

// GetByID godoc
// @Summary      Get load by ID
// @Description  Returns a single load with full details
// @Tags         loads
// @Accept       json
// @Produce      json
// @Param        id path string true "Load ID"
// @Success      200 {object} models.APIResponse{data=models.Load}
// @Failure      404 {object} models.APIResponse
// @Failure      500 {object} models.APIResponse
// @Router       /loads/{id} [get]
func (h *LoadHandler) GetByID(c echo.Context) error {
    // ... existing implementation
}

Model Annotations

Add example tags for documentation:

type CreateLoadRequest struct {
    TenderID     string          `json:"tenderId" example:"tender-123"`
    Mode         ModeOfTransport `json:"mode" example:"Dry Van"`
    CustomerRate float64         `json:"customerRate" example:"1500.00"`
    CarrierRate  *float64        `json:"carrierRate,omitempty" example:"1200.00"`
    Stops        []CreateStopRequest `json:"stops"`
}

type CreateLoadResponse struct {
    ID         string     `json:"id" example:"load-456"`
    LoadNumber string     `json:"loadNumber" example:"LD-2024-001"`
    LoadStatus LoadStatus `json:"loadStatus" example:"uncovered"`
}

For enums, add a comment listing valid values:

// LoadStatus represents the current state of a load
// @Description Load lifecycle status
// @Enum uncovered,assigned,dispatched,at_origin,in_transit,at_destination,delivered
type LoadStatus string

Regenerating Docs

Run after any annotation changes:

swag init -g cmd/server/main.go -o docs

Access docs at: http://localhost:8080/swagger/index.html

Checklist for New Endpoints

Handler:
[ ] Create handler struct with service dependency
[ ] Implement New*Handler constructor
[ ] Bind request body with c.Bind()
[ ] Use type assertion for ValidationErrors
[ ] Return APIResponse wrapper for all responses
[ ] Log errors only on 500s

Service:
[ ] Create service struct with repository dependencies
[ ] Implement New*Service constructor
[ ] Call validation function at start of create/update methods
[ ] Use context.Context throughout
[ ] Return ValidationErrors for business rule failures

Repository:
[ ] Create repository struct with database client
[ ] Use parameterized queries ($1, $2)
[ ] Check pgx.ErrNoRows for "not found"
[ ] Include deleted_at IS NULL in queries
[ ] Use transactions for multi-table operations

Models:
[ ] Add db and json tags to all fields
[ ] Use pointers for nullable fields
[ ] Include audit fields (CreatedAt, UpdatedAt, etc.)
[ ] Create request/response DTOs separate from entity

Router:
[ ] Register routes in Setup function
[ ] Use route groups for resource prefixes
[ ] Follow RESTful conventions (GET/POST/PUT/DELETE)

OpenAPI/Swagger:
[ ] Add swagger annotations to handler functions
[ ] Add example tags to request/response models
[ ] Run swag init after annotation changes
[ ] Verify docs at /swagger/index.html

Code References

  • Handlers: /internal/handlers/load.go, /internal/handlers/account.go
  • Services: /internal/services/load.go
  • Repositories: /internal/repository/load.go, /internal/repository/query_builder.go
  • Models: /internal/models/load.go, /internal/models/validation.go, /internal/models/responses.go
  • Router: /internal/router/router.go

Additional Documentation

  • Error Handling: For comprehensive error handling patterns beyond the examples shown here, see effective-go/references/error-handling.md (error wrapping, sentinel errors, custom types, panic/recover, testing)