Use when implementing API endpoints, business logic, database operations, or adding new entities to laneweaverTMS Go/Echo backend.
Implements layered backend architecture for Go/Echo APIs with handlers, services, and repositories.
npx claudepluginhub linehaul-ai/linehaulai-claude-marketplaceThis skill inherits all available tools. When active, it can use any tool Claude has access to.
HTTP Request → Handler → Service → Repository → PostgreSQL
↓ ↓ ↓
Bind/JSON Business SQL/pgx
Validate Logic Transaction
↓ ↓ ↓
HTTP Response ← Handler ← Service ← Repository
Layer Responsibilities:
/internal/handlers/): HTTP request/response, Echo context, bind JSON, return APIResponse/internal/services/): Business logic, validation, orchestration, coordinate repos/internal/repository/): SQL queries, transactions, pgx operations/internal/models/): Structs with db/json tags, enums, request/response DTOstype LoadHandler struct {
service *services.LoadService
}
func NewLoadHandler(service *services.LoadService) *LoadHandler {
return &LoadHandler{service: service}
}
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,
})
}
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,
})
}
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
}
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,
})
}
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,
}
}
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
}
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)
}
type LoadRepository struct {
db *database.Client
}
func NewLoadRepository(db *database.Client) *LoadRepository {
return &LoadRepository{db: db}
}
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
}
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
}
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
}
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
}
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 responsestring, float64, time.Time*string, *float64, *time.Timetype 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
}
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"`
}
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
}
type APIResponse struct {
Success bool `json:"success"`
Message string `json:"message,omitempty"`
Data interface{} `json:"data,omitempty"`
Errors map[string][]string `json:"errors,omitempty"`
}
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,
}
}
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
}
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)
}
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
}
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() {
// ...
}
// 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 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 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
}
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
Run after any annotation changes:
swag init -g cmd/server/main.go -o docs
Access docs at: http://localhost:8080/swagger/index.html
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
/internal/handlers/load.go, /internal/handlers/account.go/internal/services/load.go/internal/repository/load.go, /internal/repository/query_builder.go/internal/models/load.go, /internal/models/validation.go, /internal/models/responses.go/internal/router/router.goeffective-go/references/error-handling.md (error wrapping, sentinel errors, custom types, panic/recover, testing)Activates when the user asks about AI prompts, needs prompt templates, wants to search for prompts, or mentions prompts.chat. Use for discovering, retrieving, and improving prompts.
Search, retrieve, and install Agent Skills from the prompts.chat registry using MCP tools. Use when the user asks to find skills, browse skill catalogs, install a skill for Claude, or extend Claude's capabilities with reusable AI agent components.
Creating algorithmic art using p5.js with seeded randomness and interactive parameter exploration. Use this when users request creating art using code, generative art, algorithmic art, flow fields, or particle systems. Create original algorithmic art rather than copying existing artists' work to avoid copyright violations.