UNPKG

@re-shell/cli

Version:

Full-stack development platform uniting microservices and microfrontends. Build complete applications with .NET (ASP.NET Core Web API, Minimal API), Java (Spring Boot, Quarkus, Micronaut, Vert.x), Rust (Actix-Web, Warp, Rocket, Axum), Python (FastAPI, Dja

1,845 lines (1,585 loc) 55.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.goSqlxTemplate = void 0; exports.goSqlxTemplate = { id: 'go-sqlx', name: 'go-sqlx', displayName: 'Go API with sqlx', description: 'Fast Go API with sqlx for raw SQL queries with compile-time type safety', language: 'go', framework: 'chi-sqlx', version: '1.3.5', tags: ['go', 'sqlx', 'api', 'rest', 'raw-sql', 'type-safe', 'performance'], port: 8080, dependencies: {}, features: ['authentication', 'validation', 'logging', 'database', 'documentation', 'rate-limiting'], files: { // Go module configuration 'go.mod': `module {{projectName}} go 1.21 require ( github.com/go-chi/chi/v5 v5.0.11 github.com/go-chi/cors v1.2.1 github.com/go-chi/httprate v0.8.0 github.com/go-chi/jwtauth/v5 v5.3.0 github.com/go-chi/render v1.0.3 github.com/joho/godotenv v1.5.1 github.com/jmoiron/sqlx v1.3.5 github.com/lib/pq v1.10.9 github.com/go-sql-driver/mysql v1.7.1 github.com/mattn/go-sqlite3 v1.14.19 github.com/go-playground/validator/v10 v10.16.0 github.com/golang-jwt/jwt/v5 v5.2.0 github.com/golang-migrate/migrate/v4 v4.17.0 github.com/swaggo/swag v1.16.2 github.com/swaggo/http-swagger/v2 v2.0.2 github.com/rs/zerolog v1.31.0 golang.org/x/crypto v0.17.0 github.com/google/uuid v1.5.0 github.com/redis/go-redis/v9 v9.3.1 github.com/ulule/limiter/v3 v3.11.2 ) require ( github.com/KyleBanks/depth v1.2.1 // indirect github.com/PuerkitoBio/purell v1.2.1 // indirect github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect github.com/ajg/form v1.5.1 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/gabriel-vasile/mimetype v1.4.3 // indirect github.com/go-openapi/jsonpointer v0.20.2 // indirect github.com/go-openapi/jsonreference v0.20.4 // indirect github.com/go-openapi/spec v0.20.13 // indirect github.com/go-openapi/swag v0.22.7 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect github.com/goccy/go-json v0.10.2 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/leodido/go-urn v1.2.4 // indirect github.com/lestrrat-go/blackmagic v1.0.2 // indirect github.com/lestrrat-go/httpcc v1.0.1 // indirect github.com/lestrrat-go/httprc v1.0.4 // indirect github.com/lestrrat-go/iter v1.0.2 // indirect github.com/lestrrat-go/jwx/v2 v2.0.19 // indirect github.com/lestrrat-go/option v1.0.1 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/segmentio/asm v1.2.0 // indirect github.com/swaggo/files/v2 v2.0.0 // indirect go.uber.org/atomic v1.11.0 // indirect golang.org/x/net v0.19.0 // indirect golang.org/x/sync v0.5.0 // indirect golang.org/x/sys v0.15.0 // indirect golang.org/x/text v0.14.0 // indirect golang.org/x/tools v0.16.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) `, // Main application entry point 'main.go': `package main import ( "context" "fmt" "log" "net/http" "os" "os/signal" "syscall" "time" "{{projectName}}/config" "{{projectName}}/database" "{{projectName}}/handlers" "{{projectName}}/middleware" "{{projectName}}/routes" _ "{{projectName}}/docs" // swagger docs "github.com/joho/godotenv" "github.com/rs/zerolog" ) // @title {{projectName}} API // @version 1.0 // @description Fast Go API with sqlx for raw SQL queries // @contact.name API Support // @contact.email support@example.com // @license.name Apache 2.0 // @license.url http://www.apache.org/licenses/LICENSE-2.0.html // @host localhost:8080 // @BasePath /api/v1 // @securityDefinitions.apikey Bearer // @in header // @name Authorization func main() { // Load environment variables if err := godotenv.Load(); err != nil { log.Printf("Warning: .env file not found") } // Initialize configuration cfg := config.Load() // Initialize logger logger := initLogger(cfg.LogLevel) // Initialize database db, err := database.Initialize(cfg.DatabaseURL) if err != nil { logger.Fatal().Err(err).Msg("Failed to initialize database") } defer db.Close() // Run migrations if err := database.Migrate(cfg.DatabaseURL); err != nil { logger.Fatal().Err(err).Msg("Failed to run migrations") } // Initialize handlers h := handlers.New(db, logger, cfg) // Setup routes router := routes.Setup(h, cfg) // Setup server srv := &http.Server{ Addr: fmt.Sprintf(":%d", cfg.Port), Handler: router, IdleTimeout: time.Minute, ReadTimeout: 10 * time.Second, WriteTimeout: 30 * time.Second, } // Start server go func() { logger.Info().Int("port", cfg.Port).Msg("Starting server") if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { logger.Fatal().Err(err).Msg("Failed to start server") } }() // Wait for interrupt signal quit := make(chan os.Signal, 1) signal.Notify(quit, os.Interrupt, syscall.SIGTERM) <-quit logger.Info().Msg("Shutting down server...") // Graceful shutdown ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() if err := srv.Shutdown(ctx); err != nil { logger.Fatal().Err(err).Msg("Server forced to shutdown") } logger.Info().Msg("Server exited") } func initLogger(level string) *zerolog.Logger { zerolog.TimeFieldFormat = zerolog.TimeFormatUnix logLevel, err := zerolog.ParseLevel(level) if err != nil { logLevel = zerolog.InfoLevel } logger := zerolog.New(os.Stdout). Level(logLevel). With(). Timestamp(). Caller(). Logger() return &logger } `, // Configuration 'config/config.go': `package config import ( "os" "strconv" "time" ) type Config struct { Port int DatabaseURL string JWTSecret string LogLevel string RedisURL string JWTExpiry time.Duration RefreshExpiry time.Duration } func Load() *Config { return &Config{ Port: getEnvAsInt("PORT", 8080), DatabaseURL: getEnv("DATABASE_URL", "postgres://user:password@localhost/dbname?sslmode=disable"), JWTSecret: getEnv("JWT_SECRET", "your-secret-key"), LogLevel: getEnv("LOG_LEVEL", "info"), RedisURL: getEnv("REDIS_URL", "redis://localhost:6379/0"), JWTExpiry: time.Duration(getEnvAsInt("JWT_EXPIRY_MINUTES", 15)) * time.Minute, RefreshExpiry: time.Duration(getEnvAsInt("REFRESH_EXPIRY_DAYS", 7)) * 24 * time.Hour, } } func getEnv(key, defaultValue string) string { if value := os.Getenv(key); value != "" { return value } return defaultValue } func getEnvAsInt(key string, defaultValue int) int { valueStr := getEnv(key, "") if value, err := strconv.Atoi(valueStr); err == nil { return value } return defaultValue } `, // Database package 'database/database.go': `package database import ( "fmt" "time" _ "github.com/go-sql-driver/mysql" "github.com/jmoiron/sqlx" _ "github.com/lib/pq" _ "github.com/mattn/go-sqlite3" ) // Initialize creates a new database connection func Initialize(databaseURL string) (*sqlx.DB, error) { driver, err := getDriver(databaseURL) if err != nil { return nil, err } db, err := sqlx.Open(driver, databaseURL) if err != nil { return nil, fmt.Errorf("failed to open database: %w", err) } // Configure connection pool db.SetMaxOpenConns(25) db.SetMaxIdleConns(5) db.SetConnMaxLifetime(5 * time.Minute) // Verify connection if err := db.Ping(); err != nil { return nil, fmt.Errorf("failed to ping database: %w", err) } return db, nil } func getDriver(databaseURL string) (string, error) { switch { case contains(databaseURL, "postgres://") || contains(databaseURL, "postgresql://"): return "postgres", nil case contains(databaseURL, "mysql://"): return "mysql", nil case contains(databaseURL, "sqlite://") || databaseURL == ":memory:": return "sqlite3", nil default: return "", fmt.Errorf("unsupported database URL: %s", databaseURL) } } func contains(s, substr string) bool { return len(s) >= len(substr) && s[:len(substr)] == substr } `, 'database/migrations.go': `package database import ( "embed" "fmt" "github.com/golang-migrate/migrate/v4" _ "github.com/golang-migrate/migrate/v4/database/mysql" _ "github.com/golang-migrate/migrate/v4/database/postgres" _ "github.com/golang-migrate/migrate/v4/database/sqlite3" "github.com/golang-migrate/migrate/v4/source/iofs" ) //go:embed migrations/*.sql var migrations embed.FS // Migrate runs database migrations func Migrate(databaseURL string) error { driver, err := getDriver(databaseURL) if err != nil { return err } // Create source from embedded files source, err := iofs.New(migrations, "migrations") if err != nil { return fmt.Errorf("failed to create migration source: %w", err) } // Create migrator m, err := migrate.NewWithSourceInstance("iofs", source, databaseURL) if err != nil { return fmt.Errorf("failed to create migrator: %w", err) } // Run migrations if err := m.Up(); err != nil && err != migrate.ErrNoChange { return fmt.Errorf("failed to run migrations: %w", err) } return nil } `, // Migrations 'database/migrations/001_create_users_table.up.sql': `CREATE TABLE IF NOT EXISTS users ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), email VARCHAR(255) UNIQUE NOT NULL, password_hash VARCHAR(255) NOT NULL, name VARCHAR(255) NOT NULL, role VARCHAR(50) NOT NULL DEFAULT 'user', active BOOLEAN NOT NULL DEFAULT true, created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, deleted_at TIMESTAMP WITH TIME ZONE ); CREATE INDEX idx_users_email ON users(email); CREATE INDEX idx_users_deleted_at ON users(deleted_at); `, 'database/migrations/001_create_users_table.down.sql': `DROP TABLE IF EXISTS users;`, 'database/migrations/002_create_products_table.up.sql': `CREATE TABLE IF NOT EXISTS products ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), name VARCHAR(255) NOT NULL, description TEXT, price DECIMAL(10, 2) NOT NULL CHECK (price >= 0), stock INTEGER NOT NULL DEFAULT 0 CHECK (stock >= 0), category VARCHAR(100) NOT NULL, sku VARCHAR(100) UNIQUE NOT NULL, active BOOLEAN NOT NULL DEFAULT true, created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, deleted_at TIMESTAMP WITH TIME ZONE ); CREATE INDEX idx_products_sku ON products(sku); CREATE INDEX idx_products_category ON products(category); CREATE INDEX idx_products_active ON products(active); CREATE INDEX idx_products_deleted_at ON products(deleted_at); `, 'database/migrations/002_create_products_table.down.sql': `DROP TABLE IF EXISTS products;`, 'database/migrations/003_create_orders_table.up.sql': `CREATE TABLE IF NOT EXISTS orders ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES users(id), status VARCHAR(50) NOT NULL DEFAULT 'pending', total_amount DECIMAL(10, 2) NOT NULL DEFAULT 0, created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE IF NOT EXISTS order_items ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), order_id UUID NOT NULL REFERENCES orders(id) ON DELETE CASCADE, product_id UUID NOT NULL REFERENCES products(id), quantity INTEGER NOT NULL CHECK (quantity > 0), price DECIMAL(10, 2) NOT NULL, created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP ); CREATE INDEX idx_orders_user_id ON orders(user_id); CREATE INDEX idx_orders_status ON orders(status); CREATE INDEX idx_order_items_order_id ON order_items(order_id); CREATE INDEX idx_order_items_product_id ON order_items(product_id); `, 'database/migrations/003_create_orders_table.down.sql': `DROP TABLE IF EXISTS order_items; DROP TABLE IF EXISTS orders; `, // Queries 'database/queries/users.go': `package queries const ( // User queries InsertUser = \` INSERT INTO users (email, password_hash, name, role) VALUES ($1, $2, $3, $4) RETURNING id, email, name, role, active, created_at, updated_at\` GetUserByID = \` SELECT id, email, name, role, active, created_at, updated_at FROM users WHERE id = $1 AND deleted_at IS NULL\` GetUserByEmail = \` SELECT id, email, password_hash, name, role, active, created_at, updated_at FROM users WHERE email = $1 AND deleted_at IS NULL\` UpdateUser = \` UPDATE users SET email = $1, name = $2, role = $3, active = $4, updated_at = CURRENT_TIMESTAMP WHERE id = $5 AND deleted_at IS NULL RETURNING id, email, name, role, active, created_at, updated_at\` DeleteUser = \` UPDATE users SET deleted_at = CURRENT_TIMESTAMP WHERE id = $1 AND deleted_at IS NULL\` ListUsers = \` SELECT id, email, name, role, active, created_at, updated_at FROM users WHERE deleted_at IS NULL AND ($1 = '' OR name ILIKE '%' || $1 || '%' OR email ILIKE '%' || $1 || '%') AND ($2 = '' OR role = $2) ORDER BY created_at DESC LIMIT $3 OFFSET $4\` CountUsers = \` SELECT COUNT(*) FROM users WHERE deleted_at IS NULL AND ($1 = '' OR name ILIKE '%' || $1 || '%' OR email ILIKE '%' || $1 || '%') AND ($2 = '' OR role = $2)\` ) `, 'database/queries/products.go': `package queries const ( // Product queries InsertProduct = \` INSERT INTO products (name, description, price, stock, category, sku) VALUES ($1, $2, $3, $4, $5, $6) RETURNING id, name, description, price, stock, category, sku, active, created_at, updated_at\` GetProductByID = \` SELECT id, name, description, price, stock, category, sku, active, created_at, updated_at FROM products WHERE id = $1 AND deleted_at IS NULL\` UpdateProduct = \` UPDATE products SET name = $1, description = $2, price = $3, stock = $4, category = $5, sku = $6, active = $7, updated_at = CURRENT_TIMESTAMP WHERE id = $8 AND deleted_at IS NULL RETURNING id, name, description, price, stock, category, sku, active, created_at, updated_at\` UpdateProductStock = \` UPDATE products SET stock = stock + $1, updated_at = CURRENT_TIMESTAMP WHERE id = $2 AND deleted_at IS NULL RETURNING stock\` DeleteProduct = \` UPDATE products SET deleted_at = CURRENT_TIMESTAMP WHERE id = $1 AND deleted_at IS NULL\` ListProducts = \` SELECT id, name, description, price, stock, category, sku, active, created_at, updated_at FROM products WHERE deleted_at IS NULL AND ($1 = '' OR name ILIKE '%' || $1 || '%' OR description ILIKE '%' || $1 || '%') AND ($2 = '' OR category = $2) AND ($3 = 0 OR price >= $3) AND ($4 = 0 OR price <= $4) ORDER BY created_at DESC LIMIT $5 OFFSET $6\` CountProducts = \` SELECT COUNT(*) FROM products WHERE deleted_at IS NULL AND ($1 = '' OR name ILIKE '%' || $1 || '%' OR description ILIKE '%' || $1 || '%') AND ($2 = '' OR category = $2) AND ($3 = 0 OR price >= $3) AND ($4 = 0 OR price <= $4)\` ) `, // Models 'models/user.go': `package models import ( "time" "golang.org/x/crypto/bcrypt" ) type User struct { ID string \`db:"id" json:"id"\` Email string \`db:"email" json:"email"\` PasswordHash string \`db:"password_hash" json:"-"\` Name string \`db:"name" json:"name"\` Role string \`db:"role" json:"role"\` Active bool \`db:"active" json:"active"\` CreatedAt time.Time \`db:"created_at" json:"created_at"\` UpdatedAt time.Time \`db:"updated_at" json:"updated_at"\` DeletedAt *time.Time \`db:"deleted_at" json:"-"\` } func (u *User) SetPassword(password string) error { hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) if err != nil { return err } u.PasswordHash = string(hashedPassword) return nil } func (u *User) CheckPassword(password string) bool { err := bcrypt.CompareHashAndPassword([]byte(u.PasswordHash), []byte(password)) return err == nil } type CreateUserRequest struct { Email string \`json:"email" validate:"required,email"\` Password string \`json:"password" validate:"required,min=8"\` Name string \`json:"name" validate:"required,min=1,max=255"\` Role string \`json:"role" validate:"omitempty,oneof=user admin moderator"\` } type UpdateUserRequest struct { Email string \`json:"email" validate:"omitempty,email"\` Name string \`json:"name" validate:"omitempty,min=1,max=255"\` Role string \`json:"role" validate:"omitempty,oneof=user admin moderator"\` Active *bool \`json:"active" validate:"omitempty"\` } type LoginRequest struct { Email string \`json:"email" validate:"required,email"\` Password string \`json:"password" validate:"required"\` } type LoginResponse struct { AccessToken string \`json:"access_token"\` RefreshToken string \`json:"refresh_token"\` User *User \`json:"user"\` } `, 'models/product.go': `package models import ( "time" ) type Product struct { ID string \`db:"id" json:"id"\` Name string \`db:"name" json:"name"\` Description string \`db:"description" json:"description"\` Price float64 \`db:"price" json:"price"\` Stock int \`db:"stock" json:"stock"\` Category string \`db:"category" json:"category"\` SKU string \`db:"sku" json:"sku"\` Active bool \`db:"active" json:"active"\` CreatedAt time.Time \`db:"created_at" json:"created_at"\` UpdatedAt time.Time \`db:"updated_at" json:"updated_at"\` DeletedAt *time.Time \`db:"deleted_at" json:"-"\` } type CreateProductRequest struct { Name string \`json:"name" validate:"required,min=1,max=255"\` Description string \`json:"description" validate:"max=1000"\` Price float64 \`json:"price" validate:"required,min=0"\` Stock int \`json:"stock" validate:"min=0"\` Category string \`json:"category" validate:"required,min=1,max=100"\` SKU string \`json:"sku" validate:"required,min=1,max=100"\` } type UpdateProductRequest struct { Name string \`json:"name" validate:"omitempty,min=1,max=255"\` Description string \`json:"description" validate:"omitempty,max=1000"\` Price float64 \`json:"price" validate:"omitempty,min=0"\` Stock int \`json:"stock" validate:"omitempty,min=0"\` Category string \`json:"category" validate:"omitempty,min=1,max=100"\` SKU string \`json:"sku" validate:"omitempty,min=1,max=100"\` Active *bool \`json:"active" validate:"omitempty"\` } type ListProductsRequest struct { Search string \`json:"search" form:"search"\` Category string \`json:"category" form:"category"\` MinPrice float64 \`json:"min_price" form:"min_price"\` MaxPrice float64 \`json:"max_price" form:"max_price"\` Page int \`json:"page" form:"page" validate:"min=1"\` Limit int \`json:"limit" form:"limit" validate:"min=1,max=100"\` } `, 'models/pagination.go': `package models type PaginationRequest struct { Page int \`json:"page" form:"page" validate:"min=1"\` Limit int \`json:"limit" form:"limit" validate:"min=1,max=100"\` } type PaginationResponse struct { Total int \`json:"total"\` Page int \`json:"page"\` Limit int \`json:"limit"\` Pages int \`json:"pages"\` } func NewPaginationResponse(total, page, limit int) PaginationResponse { pages := total / limit if total%limit > 0 { pages++ } return PaginationResponse{ Total: total, Page: page, Limit: limit, Pages: pages, } } `, // Handlers 'handlers/handlers.go': `package handlers import ( "{{projectName}}/config" "github.com/jmoiron/sqlx" "github.com/rs/zerolog" ) type Handlers struct { db *sqlx.DB logger *zerolog.Logger config *config.Config } func New(db *sqlx.DB, logger *zerolog.Logger, config *config.Config) *Handlers { return &Handlers{ db: db, logger: logger, config: config, } } `, 'handlers/users.go': `package handlers import ( "database/sql" "encoding/json" "errors" "net/http" "{{projectName}}/database/queries" "{{projectName}}/models" "{{projectName}}/utils" "github.com/go-chi/chi/v5" "github.com/go-chi/render" "github.com/go-playground/validator/v10" "github.com/google/uuid" ) var validate = validator.New() // CreateUser godoc // @Summary Create a new user // @Description Create a new user with the provided information // @Tags users // @Accept json // @Produce json // @Param user body models.CreateUserRequest true "User information" // @Success 201 {object} models.User // @Failure 400 {object} utils.ErrorResponse // @Failure 409 {object} utils.ErrorResponse // @Router /users [post] func (h *Handlers) CreateUser(w http.ResponseWriter, r *http.Request) { var req models.CreateUserRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { render.Render(w, r, utils.ErrBadRequest(err)) return } if err := validate.Struct(req); err != nil { render.Render(w, r, utils.ErrValidation(err)) return } user := &models.User{ Email: req.Email, Name: req.Name, Role: req.Role, } if user.Role == "" { user.Role = "user" } if err := user.SetPassword(req.Password); err != nil { render.Render(w, r, utils.ErrInternalServer(err)) return } err := h.db.Get(user, queries.InsertUser, user.Email, user.PasswordHash, user.Name, user.Role) if err != nil { h.logger.Error().Err(err).Msg("Failed to create user") if utils.IsDuplicateError(err) { render.Render(w, r, utils.ErrConflict("user with this email already exists")) return } render.Render(w, r, utils.ErrInternalServer(err)) return } render.Status(r, http.StatusCreated) render.JSON(w, r, user) } // GetUser godoc // @Summary Get a user by ID // @Description Get user information by user ID // @Tags users // @Accept json // @Produce json // @Param id path string true "User ID" // @Success 200 {object} models.User // @Failure 404 {object} utils.ErrorResponse // @Security Bearer // @Router /users/{id} [get] func (h *Handlers) GetUser(w http.ResponseWriter, r *http.Request) { userID := chi.URLParam(r, "id") if _, err := uuid.Parse(userID); err != nil { render.Render(w, r, utils.ErrBadRequest(errors.New("invalid user ID format"))) return } var user models.User err := h.db.Get(&user, queries.GetUserByID, userID) if err != nil { if errors.Is(err, sql.ErrNoRows) { render.Render(w, r, utils.ErrNotFound("user not found")) return } h.logger.Error().Err(err).Str("user_id", userID).Msg("Failed to get user") render.Render(w, r, utils.ErrInternalServer(err)) return } render.JSON(w, r, user) } // UpdateUser godoc // @Summary Update a user // @Description Update user information // @Tags users // @Accept json // @Produce json // @Param id path string true "User ID" // @Param user body models.UpdateUserRequest true "User update information" // @Success 200 {object} models.User // @Failure 400 {object} utils.ErrorResponse // @Failure 404 {object} utils.ErrorResponse // @Security Bearer // @Router /users/{id} [put] func (h *Handlers) UpdateUser(w http.ResponseWriter, r *http.Request) { userID := chi.URLParam(r, "id") if _, err := uuid.Parse(userID); err != nil { render.Render(w, r, utils.ErrBadRequest(errors.New("invalid user ID format"))) return } var req models.UpdateUserRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { render.Render(w, r, utils.ErrBadRequest(err)) return } if err := validate.Struct(req); err != nil { render.Render(w, r, utils.ErrValidation(err)) return } // Get existing user var user models.User err := h.db.Get(&user, queries.GetUserByID, userID) if err != nil { if errors.Is(err, sql.ErrNoRows) { render.Render(w, r, utils.ErrNotFound("user not found")) return } h.logger.Error().Err(err).Str("user_id", userID).Msg("Failed to get user") render.Render(w, r, utils.ErrInternalServer(err)) return } // Update fields if req.Email != "" { user.Email = req.Email } if req.Name != "" { user.Name = req.Name } if req.Role != "" { user.Role = req.Role } if req.Active != nil { user.Active = *req.Active } // Execute update err = h.db.Get(&user, queries.UpdateUser, user.Email, user.Name, user.Role, user.Active, userID) if err != nil { h.logger.Error().Err(err).Str("user_id", userID).Msg("Failed to update user") if utils.IsDuplicateError(err) { render.Render(w, r, utils.ErrConflict("user with this email already exists")) return } render.Render(w, r, utils.ErrInternalServer(err)) return } render.JSON(w, r, user) } // DeleteUser godoc // @Summary Delete a user // @Description Soft delete a user by ID // @Tags users // @Accept json // @Produce json // @Param id path string true "User ID" // @Success 204 "No Content" // @Failure 404 {object} utils.ErrorResponse // @Security Bearer // @Router /users/{id} [delete] func (h *Handlers) DeleteUser(w http.ResponseWriter, r *http.Request) { userID := chi.URLParam(r, "id") if _, err := uuid.Parse(userID); err != nil { render.Render(w, r, utils.ErrBadRequest(errors.New("invalid user ID format"))) return } result, err := h.db.Exec(queries.DeleteUser, userID) if err != nil { h.logger.Error().Err(err).Str("user_id", userID).Msg("Failed to delete user") render.Render(w, r, utils.ErrInternalServer(err)) return } rowsAffected, err := result.RowsAffected() if err != nil { render.Render(w, r, utils.ErrInternalServer(err)) return } if rowsAffected == 0 { render.Render(w, r, utils.ErrNotFound("user not found")) return } w.WriteHeader(http.StatusNoContent) } // ListUsers godoc // @Summary List users // @Description Get a paginated list of users // @Tags users // @Accept json // @Produce json // @Param search query string false "Search term" // @Param role query string false "Filter by role" // @Param page query int false "Page number" default(1) // @Param limit query int false "Items per page" default(10) // @Success 200 {object} map[string]interface{} // @Security Bearer // @Router /users [get] func (h *Handlers) ListUsers(w http.ResponseWriter, r *http.Request) { search := r.URL.Query().Get("search") role := r.URL.Query().Get("role") page := utils.ParseIntOrDefault(r.URL.Query().Get("page"), 1) limit := utils.ParseIntOrDefault(r.URL.Query().Get("limit"), 10) if page < 1 { page = 1 } if limit < 1 || limit > 100 { limit = 10 } offset := (page - 1) * limit // Get total count var total int err := h.db.Get(&total, queries.CountUsers, search, role) if err != nil { h.logger.Error().Err(err).Msg("Failed to count users") render.Render(w, r, utils.ErrInternalServer(err)) return } // Get users var users []models.User err = h.db.Select(&users, queries.ListUsers, search, role, limit, offset) if err != nil { h.logger.Error().Err(err).Msg("Failed to list users") render.Render(w, r, utils.ErrInternalServer(err)) return } pagination := models.NewPaginationResponse(total, page, limit) render.JSON(w, r, map[string]interface{}{ "users": users, "pagination": pagination, }) } // Login godoc // @Summary User login // @Description Authenticate user and return JWT tokens // @Tags auth // @Accept json // @Produce json // @Param credentials body models.LoginRequest true "Login credentials" // @Success 200 {object} models.LoginResponse // @Failure 401 {object} utils.ErrorResponse // @Router /auth/login [post] func (h *Handlers) Login(w http.ResponseWriter, r *http.Request) { var req models.LoginRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { render.Render(w, r, utils.ErrBadRequest(err)) return } if err := validate.Struct(req); err != nil { render.Render(w, r, utils.ErrValidation(err)) return } var user models.User err := h.db.Get(&user, queries.GetUserByEmail, req.Email) if err != nil { if errors.Is(err, sql.ErrNoRows) { render.Render(w, r, utils.ErrUnauthorized("invalid credentials")) return } h.logger.Error().Err(err).Msg("Failed to get user by email") render.Render(w, r, utils.ErrInternalServer(err)) return } if !user.CheckPassword(req.Password) { render.Render(w, r, utils.ErrUnauthorized("invalid credentials")) return } if !user.Active { render.Render(w, r, utils.ErrForbidden("account is inactive")) return } // Generate tokens accessToken, err := utils.GenerateToken(user.ID, "access", h.config.JWTSecret, h.config.JWTExpiry) if err != nil { h.logger.Error().Err(err).Msg("Failed to generate access token") render.Render(w, r, utils.ErrInternalServer(err)) return } refreshToken, err := utils.GenerateToken(user.ID, "refresh", h.config.JWTSecret, h.config.RefreshExpiry) if err != nil { h.logger.Error().Err(err).Msg("Failed to generate refresh token") render.Render(w, r, utils.ErrInternalServer(err)) return } response := models.LoginResponse{ AccessToken: accessToken, RefreshToken: refreshToken, User: &user, } render.JSON(w, r, response) } `, 'handlers/products.go': `package handlers import ( "database/sql" "encoding/json" "errors" "net/http" "{{projectName}}/database/queries" "{{projectName}}/models" "{{projectName}}/utils" "github.com/go-chi/chi/v5" "github.com/go-chi/render" "github.com/google/uuid" ) // CreateProduct godoc // @Summary Create a new product // @Description Create a new product with the provided information // @Tags products // @Accept json // @Produce json // @Param product body models.CreateProductRequest true "Product information" // @Success 201 {object} models.Product // @Failure 400 {object} utils.ErrorResponse // @Failure 409 {object} utils.ErrorResponse // @Security Bearer // @Router /products [post] func (h *Handlers) CreateProduct(w http.ResponseWriter, r *http.Request) { var req models.CreateProductRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { render.Render(w, r, utils.ErrBadRequest(err)) return } if err := validate.Struct(req); err != nil { render.Render(w, r, utils.ErrValidation(err)) return } var product models.Product err := h.db.Get(&product, queries.InsertProduct, req.Name, req.Description, req.Price, req.Stock, req.Category, req.SKU) if err != nil { h.logger.Error().Err(err).Msg("Failed to create product") if utils.IsDuplicateError(err) { render.Render(w, r, utils.ErrConflict("product with this SKU already exists")) return } render.Render(w, r, utils.ErrInternalServer(err)) return } render.Status(r, http.StatusCreated) render.JSON(w, r, product) } // GetProduct godoc // @Summary Get a product by ID // @Description Get product information by product ID // @Tags products // @Accept json // @Produce json // @Param id path string true "Product ID" // @Success 200 {object} models.Product // @Failure 404 {object} utils.ErrorResponse // @Router /products/{id} [get] func (h *Handlers) GetProduct(w http.ResponseWriter, r *http.Request) { productID := chi.URLParam(r, "id") if _, err := uuid.Parse(productID); err != nil { render.Render(w, r, utils.ErrBadRequest(errors.New("invalid product ID format"))) return } var product models.Product err := h.db.Get(&product, queries.GetProductByID, productID) if err != nil { if errors.Is(err, sql.ErrNoRows) { render.Render(w, r, utils.ErrNotFound("product not found")) return } h.logger.Error().Err(err).Str("product_id", productID).Msg("Failed to get product") render.Render(w, r, utils.ErrInternalServer(err)) return } render.JSON(w, r, product) } // UpdateProduct godoc // @Summary Update a product // @Description Update product information // @Tags products // @Accept json // @Produce json // @Param id path string true "Product ID" // @Param product body models.UpdateProductRequest true "Product update information" // @Success 200 {object} models.Product // @Failure 400 {object} utils.ErrorResponse // @Failure 404 {object} utils.ErrorResponse // @Security Bearer // @Router /products/{id} [put] func (h *Handlers) UpdateProduct(w http.ResponseWriter, r *http.Request) { productID := chi.URLParam(r, "id") if _, err := uuid.Parse(productID); err != nil { render.Render(w, r, utils.ErrBadRequest(errors.New("invalid product ID format"))) return } var req models.UpdateProductRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { render.Render(w, r, utils.ErrBadRequest(err)) return } if err := validate.Struct(req); err != nil { render.Render(w, r, utils.ErrValidation(err)) return } // Get existing product var product models.Product err := h.db.Get(&product, queries.GetProductByID, productID) if err != nil { if errors.Is(err, sql.ErrNoRows) { render.Render(w, r, utils.ErrNotFound("product not found")) return } h.logger.Error().Err(err).Str("product_id", productID).Msg("Failed to get product") render.Render(w, r, utils.ErrInternalServer(err)) return } // Update fields if req.Name != "" { product.Name = req.Name } if req.Description != "" { product.Description = req.Description } if req.Price > 0 { product.Price = req.Price } if req.Stock >= 0 { product.Stock = req.Stock } if req.Category != "" { product.Category = req.Category } if req.SKU != "" { product.SKU = req.SKU } if req.Active != nil { product.Active = *req.Active } // Execute update err = h.db.Get(&product, queries.UpdateProduct, product.Name, product.Description, product.Price, product.Stock, product.Category, product.SKU, product.Active, productID) if err != nil { h.logger.Error().Err(err).Str("product_id", productID).Msg("Failed to update product") if utils.IsDuplicateError(err) { render.Render(w, r, utils.ErrConflict("product with this SKU already exists")) return } render.Render(w, r, utils.ErrInternalServer(err)) return } render.JSON(w, r, product) } // DeleteProduct godoc // @Summary Delete a product // @Description Soft delete a product by ID // @Tags products // @Accept json // @Produce json // @Param id path string true "Product ID" // @Success 204 "No Content" // @Failure 404 {object} utils.ErrorResponse // @Security Bearer // @Router /products/{id} [delete] func (h *Handlers) DeleteProduct(w http.ResponseWriter, r *http.Request) { productID := chi.URLParam(r, "id") if _, err := uuid.Parse(productID); err != nil { render.Render(w, r, utils.ErrBadRequest(errors.New("invalid product ID format"))) return } result, err := h.db.Exec(queries.DeleteProduct, productID) if err != nil { h.logger.Error().Err(err).Str("product_id", productID).Msg("Failed to delete product") render.Render(w, r, utils.ErrInternalServer(err)) return } rowsAffected, err := result.RowsAffected() if err != nil { render.Render(w, r, utils.ErrInternalServer(err)) return } if rowsAffected == 0 { render.Render(w, r, utils.ErrNotFound("product not found")) return } w.WriteHeader(http.StatusNoContent) } // ListProducts godoc // @Summary List products // @Description Get a paginated list of products // @Tags products // @Accept json // @Produce json // @Param search query string false "Search term" // @Param category query string false "Filter by category" // @Param min_price query number false "Minimum price" // @Param max_price query number false "Maximum price" // @Param page query int false "Page number" default(1) // @Param limit query int false "Items per page" default(10) // @Success 200 {object} map[string]interface{} // @Router /products [get] func (h *Handlers) ListProducts(w http.ResponseWriter, r *http.Request) { req := models.ListProductsRequest{ Search: r.URL.Query().Get("search"), Category: r.URL.Query().Get("category"), MinPrice: utils.ParseFloatOrDefault(r.URL.Query().Get("min_price"), 0), MaxPrice: utils.ParseFloatOrDefault(r.URL.Query().Get("max_price"), 0), Page: utils.ParseIntOrDefault(r.URL.Query().Get("page"), 1), Limit: utils.ParseIntOrDefault(r.URL.Query().Get("limit"), 10), } if req.Page < 1 { req.Page = 1 } if req.Limit < 1 || req.Limit > 100 { req.Limit = 10 } offset := (req.Page - 1) * req.Limit // Get total count var total int err := h.db.Get(&total, queries.CountProducts, req.Search, req.Category, req.MinPrice, req.MaxPrice) if err != nil { h.logger.Error().Err(err).Msg("Failed to count products") render.Render(w, r, utils.ErrInternalServer(err)) return } // Get products var products []models.Product err = h.db.Select(&products, queries.ListProducts, req.Search, req.Category, req.MinPrice, req.MaxPrice, req.Limit, offset) if err != nil { h.logger.Error().Err(err).Msg("Failed to list products") render.Render(w, r, utils.ErrInternalServer(err)) return } pagination := models.NewPaginationResponse(total, req.Page, req.Limit) render.JSON(w, r, map[string]interface{}{ "products": products, "pagination": pagination, }) } `, // Routes 'routes/routes.go': `package routes import ( "net/http" "{{projectName}}/config" "{{projectName}}/handlers" "{{projectName}}/middleware" "github.com/go-chi/chi/v5" chiMiddleware "github.com/go-chi/chi/v5/middleware" "github.com/go-chi/cors" httpSwagger "github.com/swaggo/http-swagger/v2" ) func Setup(h *handlers.Handlers, cfg *config.Config) chi.Router { r := chi.NewRouter() // Middleware r.Use(chiMiddleware.RequestID) r.Use(chiMiddleware.RealIP) r.Use(chiMiddleware.Logger) r.Use(chiMiddleware.Recoverer) r.Use(chiMiddleware.Compress(5)) // CORS r.Use(cors.Handler(cors.Options{ AllowedOrigins: []string{"http://localhost:*", "https://localhost:*"}, AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"}, AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token"}, ExposedHeaders: []string{"Link"}, AllowCredentials: true, MaxAge: 300, })) // Rate limiting r.Use(middleware.RateLimiter(cfg.RedisURL)) // Health check r.Get("/health", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) w.Write([]byte("OK")) }) // Swagger documentation r.Get("/swagger/*", httpSwagger.Handler( httpSwagger.URL("/swagger/doc.json"), )) // API routes r.Route("/api/v1", func(r chi.Router) { // Public routes r.Group(func(r chi.Router) { // Auth r.Post("/auth/login", h.Login) r.Post("/users", h.CreateUser) // Products (public read) r.Get("/products", h.ListProducts) r.Get("/products/{id}", h.GetProduct) }) // Protected routes r.Group(func(r chi.Router) { r.Use(middleware.JWTAuth(cfg.JWTSecret)) // Users r.Get("/users", h.ListUsers) r.Get("/users/{id}", h.GetUser) r.Put("/users/{id}", h.UpdateUser) r.Delete("/users/{id}", h.DeleteUser) // Products (protected write) r.Post("/products", h.CreateProduct) r.Put("/products/{id}", h.UpdateProduct) r.Delete("/products/{id}", h.DeleteProduct) }) }) return r } `, // Middleware 'middleware/auth.go': `package middleware import ( "context" "net/http" "strings" "{{projectName}}/utils" "github.com/go-chi/jwtauth/v5" "github.com/go-chi/render" ) func JWTAuth(secret string) func(http.Handler) http.Handler { tokenAuth := jwtauth.New("HS256", []byte(secret), nil) return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { token, claims, err := jwtauth.FromContext(r.Context()) if err != nil { render.Render(w, r, utils.ErrUnauthorized("missing or invalid token")) return } if token == nil || !token.Valid { render.Render(w, r, utils.ErrUnauthorized("invalid token")) return } // Check token type tokenType, ok := claims["type"].(string) if !ok || tokenType != "access" { render.Render(w, r, utils.ErrUnauthorized("invalid token type")) return } // Add user ID to context userID, ok := claims["user_id"].(string) if !ok { render.Render(w, r, utils.ErrUnauthorized("invalid token claims")) return } ctx := context.WithValue(r.Context(), "user_id", userID) next.ServeHTTP(w, r.WithContext(ctx)) }) } } func GetUserIDFromContext(ctx context.Context) string { userID, _ := ctx.Value("user_id").(string) return userID } `, 'middleware/rate_limiter.go': `package middleware import ( "context" "net/http" "time" "{{projectName}}/utils" "github.com/go-chi/httprate" "github.com/go-chi/render" "github.com/redis/go-redis/v9" ) func RateLimiter(redisURL string) func(http.Handler) http.Handler { // Parse Redis URL opt, err := redis.ParseURL(redisURL) if err != nil { // Fallback to in-memory rate limiter return httprate.LimitByIP(100, 1*time.Minute) } // Create Redis client client := redis.NewClient(opt) // Test connection ctx := context.Background() if err := client.Ping(ctx).Err(); err != nil { // Fallback to in-memory rate limiter return httprate.LimitByIP(100, 1*time.Minute) } // Use Redis-backed rate limiter return httprate.Limit( 100, 1*time.Minute, httprate.WithKeyFuncs(httprate.KeyByIP), httprate.WithLimitHandler(func(w http.ResponseWriter, r *http.Request) { render.Render(w, r, utils.ErrTooManyRequests("rate limit exceeded")) }), ) } `, // Utils 'utils/errors.go': `package utils import ( "fmt" "net/http" "strings" "github.com/go-chi/render" "github.com/go-playground/validator/v10" ) type ErrorResponse struct { Err error \`json:"-"\` HTTPStatusCode int \`json:"-"\` StatusText string \`json:"status"\` AppCode int64 \`json:"code,omitempty"\` ErrorText string \`json:"error,omitempty"\` } func (e *ErrorResponse) Render(w http.ResponseWriter, r *http.Request) error { render.Status(r, e.HTTPStatusCode) return nil } func ErrBadRequest(err error) render.Renderer { return &ErrorResponse{ Err: err, HTTPStatusCode: http.StatusBadRequest, StatusText: "Bad request", ErrorText: err.Error(), } } func ErrValidation(err error) render.Renderer { var errText string if validationErrors, ok := err.(validator.ValidationErrors); ok { var errors []string for _, e := range validationErrors { errors = append(errors, formatValidationError(e)) } errText = strings.Join(errors, "; ") } else { errText = err.Error() } return &ErrorResponse{ Err: err, HTTPStatusCode: http.StatusBadRequest, StatusText: "Validation failed", ErrorText: errText, } } func ErrUnauthorized(message string) render.Renderer { return &ErrorResponse{ HTTPStatusCode: http.StatusUnauthorized, StatusText: "Unauthorized", ErrorText: message, } } func ErrForbidden(message string) render.Renderer { return &ErrorResponse{ HTTPStatusCode: http.StatusForbidden, StatusText: "Forbidden", ErrorText: message, } } func ErrNotFound(message string) render.Renderer { return &ErrorResponse{ HTTPStatusCode: http.StatusNotFound, StatusText: "Not found", ErrorText: message, } } func ErrConflict(message string) render.Renderer { return &ErrorResponse{ HTTPStatusCode: http.StatusConflict, StatusText: "Conflict", ErrorText: message, } } func ErrInternalServer(err error) render.Renderer { return &ErrorResponse{ Err: err, HTTPStatusCode: http.StatusInternalServerError, StatusText: "Internal server error", ErrorText: "An error occurred processing your request", } } func ErrTooManyRequests(message string) render.Renderer { return &ErrorResponse{ HTTPStatusCode: http.StatusTooManyRequests, StatusText: "Too many requests", ErrorText: message, } } func formatValidationError(e validator.FieldError) string { switch e.Tag() { case "required": return fmt.Sprintf("%s is required", e.Field()) case "email": return fmt.Sprintf("%s must be a valid email", e.Field()) case "min": return fmt.Sprintf("%s must be at least %s", e.Field(), e.Param()) case "max": return fmt.Sprintf("%s must be at most %s", e.Field(), e.Param()) default: return fmt.Sprintf("%s is invalid", e.Field()) } } func IsDuplicateError(err error) bool { return strings.Contains(err.Error(), "duplicate key") || strings.Contains(err.Error(), "UNIQUE constraint") || strings.Contains(err.Error(), "Duplicate entry") } `, 'utils/jwt.go': `package utils import ( "time" "github.com/golang-jwt/jwt/v5" "github.com/google/uuid" ) func GenerateToken(userID, tokenType, secret string, expiry time.Duration) (string, error) { claims := jwt.MapClaims{ "user_id": userID, "type": tokenType, "exp": time.Now().Add(expiry).Unix(), "iat": time.Now().Unix(), "jti": uuid.New().String(), } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) return token.SignedString([]byte(secret)) } func ValidateToken(tokenString, secret string) (*jwt.Token, jwt.MapClaims, error) { token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { return nil, jwt.ErrSignatureInvalid } return []byte(secret), nil }) if err != nil { return nil, nil, err } claims, ok := token.Claims.(jwt.MapClaims) if !ok || !token.Valid { return nil, nil, jwt.ErrTokenInvalidClaims } return token, claims, nil } `, 'utils/helpers.go': `package utils import ( "strconv" ) func ParseIntOrDefault(s string, defaultValue int) int { if value, err := strconv.Atoi(s); err == nil { return value } return defaultValue } func ParseFloatOrDefault(s string, defaultValue float64) float64 { if value, err := strconv.ParseFloat(s, 64); err == nil { return value } return defaultValue } `, // Docker configuration 'Dockerfile': `# Build stage FROM golang:1.21-alpine AS builder # Install build dependencies RUN apk add --no-cache git ca-certificates # Set working directory WORKDIR /app # Copy go mod files COPY go.mod go.sum ./ # Download dependencies RUN go mod download # Copy source code COPY . . # Generate swagger docs RUN go install github.com/swaggo/swag/cmd/swag@latest RUN swag init # Build the application RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o server . # Final stage FROM alpine:latest # Install runtime dependencies RUN apk --no-cache add ca-certificates tzdata # Create non-root user RUN addgroup -g 1000 -S app && \ adduser -u 1000 -S app -G app # Set working directory WORKDIR /app # Copy binary from builder COPY --from=builder /app/server . COPY --from=builder /app/docs ./docs COPY --chown=app:app .env.example .env # Copy migration files COPY --from=builder /app/database/migrations ./database/migrations # Change ownership RUN chown -R app:app /app # Switch to non-root user USER app # Expose port EXPOSE 8080 # Health check HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ CMD wget --no-verbose --tries=1 --spider http://localhost:8080/health || exit 1 # Run the application CMD ["./server"] `, 'docker-compose.yml': `version: '3.8' services: app: build: . ports: - "8080:8080" environment: - PORT=8080 - DATABASE_URL=postgres://postgres:password@postgres:5432/{{projectName}}?sslmode=disable - JWT_SECRET=your-secret-key - LOG_LEVEL=info - REDIS_URL=redis://redis:6379/0 depends_on: postgres: condition: service_healthy redis: condition: service_healthy networks: - app-network restart: unless-stopped postgres: image: postgres:16-alpine ports: - "5432:5432" environment: - POSTGRES_USER=postgres - POSTGRES_PASSWORD=password - POSTGRES_DB={{projectName}} volumes: - postgres-data:/var/lib/postgresql/data networks: - app-network healthcheck: test: ["CMD-SHELL", "pg_isready -U postgres"] interval: 10s timeout: 5s retries: 5 redis: image: redis:7-alpine ports: - "6379:6379" volumes: - redis-data:/data networks: - app-network healthcheck: test: ["CMD", "redis-cli", "ping"] interval: 10s timeout: 5s retries: 5 volumes: postgres-data: redis-data: networks: app-network: driver: bridge `, // Makefile 'Makefile': `.PHONY: all build run test clean migrate swagger lint fmt vet # Variables BINARY_NAME=server DOCKER_IMAGE={{projectName}}:latest GO_FILES=$(shell find . -name '*.go' -type f) # Build the application all: clean build build: @echo "Building..." go build -o $(BINARY_NAME) -v # Run the application run: swagger build @echo "Running..." ./$(BINARY_NAME) # Run with hot reload dev: @echo "Running with hot reload..." air # Clean build artifacts clean: @echo "Cleaning..." go clean rm -f $(BINARY_NAME) # Run tests test: @echo "Running tests..." go test -v -race -cover ./... # Run tests with coverage test-coverage: @echo "Running tests with coverage..." go test -v -race -coverprofile=coverage.out ./... go tool cover -html=coverage.out -o coverage.html # Generate swagger documentation swagger: @echo "Generating swagger docs..." swag init # Run database migrations migrate-up: @echo "Running migrations..." migrate -path database/migrations -database "$\${DATABASE_URL}" up migrate-down: @echo "Rolling back migrations..." migrate -path database/migrations -database "$\${DATABASE_URL}" down migrate-create: @echo "Creating new migration..." migrate create -ext