diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..1964e92 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,38 @@ +# Ignore the Go binary (if it exists locally) +go-api-template + +# Ignore dependency directories +vendor/ +go.mod +go.sum + +# Ignore IDE and editor files +.idea/ +.vscode/ +*.swp +*.swo + +# Ignore build and test artifacts +bin/ +pkg/ +*.test +*.out + +# Ignore log files +*.log + +# Ignore Git files +.git/ +.gitignore + +# Ignore Docker files (if any) +Dockerfile +.dockerignore + +# Ignore temporary files +tmp/ +*.tmp + +# Ignore environment-specific files +.env +.env.local diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..f969979 --- /dev/null +++ b/.env.example @@ -0,0 +1,4 @@ +# dont use "" in DATABASE_URI and CONFIG_PATH + +DATABASE_URI= +CONFIG_PATH=config/local.yaml diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml new file mode 100644 index 0000000..943c835 --- /dev/null +++ b/.github/workflows/ci-cd.yml @@ -0,0 +1,60 @@ +name: CI/CD Pipeline + +on: + push: + branches: + - main + pull_request: + branches: + - main + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Set up Go + uses: actions/setup-go@v4 + with: + go-version: 1.20 + + - name: Run tests + run: go test -v ./... + + build: + runs-on: ubuntu-latest + needs: test + steps: + - uses: actions/checkout@v3 + + - name: Set up Go + uses: actions/setup-go@v4 + with: + go-version: 1.20 + + - name: Build the application + run: go build -o bin/go-api-template cmd/go-api-template/main.go + + deploy: + runs-on: ubuntu-latest + needs: build + if: github.ref == 'refs/heads/main' + steps: + - uses: actions/checkout@v3 + + - name: Set up Go + uses: actions/setup-go@v4 + with: + go-version: 1.20 + + - name: Build the application + run: go build -o bin/go-api-template cmd/go-api-template/main.go + + - name: Deploy to Heroku (example) + env: + HEROKU_API_KEY: ${{ secrets.HEROKU_API_KEY }} + run: | + heroku container:login + heroku container:push web -a your-heroku-app-name + heroku container:release web -a your-heroku-app-name diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..bed5230 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,30 @@ +# Use the official Golang image to create a build artifact. +FROM golang:1.23.4 as builder + +# Set the working directory inside the container. +WORKDIR /app + +# Copy the Go module files and download dependencies. +COPY go.mod go.sum ./ +RUN go mod download + +# Copy the rest of the application code. +COPY . . + +# Build the Go application. +RUN CGO_ENABLED=0 GOOS=linux go build -o go-api-template . + +# Use a minimal Alpine image for the final stage. +FROM alpine:latest + +# Set the working directory. +WORKDIR /root/ + +# Copy the binary from the builder stage. +COPY --from=builder /app/go-api-template . + +# Expose the port the app runs on. +EXPOSE 8080 + +# Command to run the application. +CMD ["./go-api-template"] diff --git a/README.md b/README.md index d64e9ae..73bba4b 100644 --- a/README.md +++ b/README.md @@ -1 +1,56 @@ -# go-api-template \ No newline at end of file +# Go API Template + +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) + +This is a production-ready Go backend API template designed to kickstart your next project. + +## Features + +- **Multiple Database Support**: Easily switch between different databases (e.g., PostgreSQL, MySQL, SQLite). +- **Docker Integration**: Run the application and its dependencies in isolated containers. +- **Makefile**: Simplify common tasks like building, testing, and running the application. +- **Database Migrations**: Manage database schema changes with ease. +- **Configuration Management**: Use `local.yaml` for environment-specific configurations. +- **Modular Structure**: Organized into `models`, `database`, `handlers`, `middleware`, `repositories`, and `services`. +- **Testing System**: Includes a robust testing framework for unit and integration tests. + +## Project Structure + +## Prerequisites + +- Go 1.23 or higher +- Docker and Docker Compose +- Make file + +### 1. Create a New Repository + +Click the **"Use this template"** button at the top right of this repository to create your own copy. + +### 2. Configure the Application + +Update the following files with your environment-specific settings: + +- **`config/local.yaml`**: Add your application-specific configurations (e.g., server port, logging level). +- **`.env`**: Add your environment variables (e.g., database credentials, API keys). + +### 3. Download Dependencies + +`go mod download` + +### 4. Run Your App + +`make run` + +### 5. Database Migrations + +- To apply migrations: `make migrate-up` +- To rollback migrations: `make migrate-down` + +## Contributing + +Contributions are welcome! Please open an issue or submit a pull request for any improvements or bug fixes. + +## License + +This project is licensed under the **MIT License**. See the [LICENSE](LICENSE) file for details. + diff --git a/cmd/go-api-template/main.go b/cmd/go-api-template/main.go new file mode 100644 index 0000000..d731d86 --- /dev/null +++ b/cmd/go-api-template/main.go @@ -0,0 +1,72 @@ +package main + +import ( + "context" + "log" + "log/slog" + "net/http" + "os" + "os/signal" + "syscall" + "time" + + "github.com/gauravst/go-api-template/internal/api/handlers" + "github.com/gauravst/go-api-template/internal/api/middleware" + "github.com/gauravst/go-api-template/internal/config" + "github.com/gauravst/go-api-template/internal/database" + "github.com/gauravst/go-api-template/internal/repositories" + "github.com/gauravst/go-api-template/internal/services" +) + +func main() { + // load config + cfg := config.ConfigMustLoad() + + // database setup + database.InitDB(cfg.DatabaseUri) + defer database.CloseDB() + + //setup router + router := http.NewServeMux() + + userRepo := repositories.NewUserRepository(database.DB) + userService := services.NewUserService(userRepo) + + router.HandleFunc("GET /api/user", middleware.Auth(handlers.GetUser(userService))) + router.HandleFunc("POST /api/user", handlers.CreateUser(userService)) + router.HandleFunc("PUT /api/user", middleware.Auth(handlers.UpdateUser(userService))) + router.HandleFunc("DELETE /api/user", middleware.Auth(handlers.DeleteUser(userService))) + + // setup server + server := &http.Server{ + Addr: cfg.Address, + Handler: router, + } + + slog.Info("server started", slog.String("address", cfg.Address)) + + done := make(chan os.Signal, 1) + + signal.Notify(done, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + + go func() { + err := server.ListenAndServe() + if err != nil { + log.Fatal("failed to start server") + } + }() + + <-done + + slog.Info("shutting down the server") + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + err := server.Shutdown(ctx) + if err != nil { + slog.Error("faild to Shutdown server", slog.String("error", err.Error())) + } + + slog.Info("server Shutdown successfully") +} diff --git a/config/local.yaml b/config/local.yaml new file mode 100644 index 0000000..09576d6 --- /dev/null +++ b/config/local.yaml @@ -0,0 +1,4 @@ +env: "dev" +http_server: + address: "localhost:8080" + port : 8080 diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..7161558 --- /dev/null +++ b/go.mod @@ -0,0 +1,21 @@ +module github.com/gauravst/go-api-template + +go 1.23.4 + +require ( + github.com/BurntSushi/toml v1.2.1 // indirect + github.com/gabriel-vasile/mimetype v1.4.8 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.24.0 // indirect + github.com/ilyakaznacheev/cleanenv v1.5.0 // indirect + github.com/joho/godotenv v1.5.1 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/lib/pq v1.10.9 // indirect + golang.org/x/crypto v0.32.0 // indirect + golang.org/x/net v0.34.0 // indirect + golang.org/x/sys v0.29.0 // indirect + golang.org/x/text v0.21.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..30992cc --- /dev/null +++ b/go.sum @@ -0,0 +1,31 @@ +github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak= +github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= +github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.24.0 h1:KHQckvo8G6hlWnrPX4NJJ+aBfWNAE/HH+qdL2cBpCmg= +github.com/go-playground/validator/v10 v10.24.0/go.mod h1:GGzBIJMuE98Ic/kJsBXbz1x/7cByt++cQ+YOuDM5wus= +github.com/ilyakaznacheev/cleanenv v1.5.0 h1:0VNZXggJE2OYdXE87bfSSwGxeiGt9moSR2lOrsHHvr4= +github.com/ilyakaznacheev/cleanenv v1.5.0/go.mod h1:a5aDzaJrLCQZsazHol1w8InnDcOX0OColm64SlIi6gk= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= +golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= +golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= +golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= +golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= +golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3 h1:slmdOY3vp8a7KQbHkL+FLbvbkgMqmXojpFUO/jENuqQ= +olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3/go.mod h1:oVgVk4OWVDi43qWBEyGhXgYxt7+ED4iYNpTngSLX2Iw= diff --git a/internal/api/handlers/user_handler.go b/internal/api/handlers/user_handler.go new file mode 100644 index 0000000..a93c3d0 --- /dev/null +++ b/internal/api/handlers/user_handler.go @@ -0,0 +1,69 @@ +package handlers + +import ( + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + + "github.com/gauravst/go-api-template/internal/models" + "github.com/gauravst/go-api-template/internal/services" + "github.com/gauravst/go-api-template/internal/utils/response" + "github.com/go-playground/validator/v10" +) + +func CreateUser(userService services.UserService) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + + var user models.User + + err := json.NewDecoder(r.Body).Decode(&user) + if errors.Is(err, io.EOF) { + response.WriteJson(w, http.StatusBadRequest, response.GeneralError(fmt.Errorf("empty body"))) + return + } + + if err != nil { + response.WriteJson(w, http.StatusBadRequest, response.GeneralError(err)) + return + } + + // Request validation + err = validator.New().Struct(user) + if err != nil { + validateErrs := err.(validator.ValidationErrors) + response.WriteJson(w, http.StatusBadRequest, response.ValidationError(validateErrs)) + return + } + + // call here services + + err = userService.CreateUser(user) + if err != nil { + response.WriteJson(w, http.StatusInternalServerError, response.GeneralError(err)) + return + } + + // return response + response.WriteJson(w, http.StatusCreated, map[string]string{"success": "ok"}) + } +} + +func GetUser(userService services.UserService) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + + } +} + +func UpdateUser(userService services.UserService) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + + } +} + +func DeleteUser(userService services.UserService) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + + } +} diff --git a/internal/api/handlers/user_handler_test.go b/internal/api/handlers/user_handler_test.go new file mode 100644 index 0000000..338204e --- /dev/null +++ b/internal/api/handlers/user_handler_test.go @@ -0,0 +1,7 @@ +package handlers_test + +import "testing" + +func TestCreateUser(t *testing.T) { + +} diff --git a/internal/api/middleware/auth_middleware.go b/internal/api/middleware/auth_middleware.go new file mode 100644 index 0000000..617610b --- /dev/null +++ b/internal/api/middleware/auth_middleware.go @@ -0,0 +1,26 @@ +package middleware + +import ( + "net/http" +) + +func Auth(next http.Handler) http.HandlerFunc { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Extract the token from the request headers + token := r.Header.Get("Authorization") + + if !isValidToken(token) { + http.Error(w, "Unauthorized", http.StatusUnauthorized) + return + } + + // If the token is valid, call the next handler + next.ServeHTTP(w, r) + }) +} + +func isValidToken(token string) bool { + // In a real application, you would validate the token against a database or a JWT library + // For this example, we'll assume the token is valid if it's not empty + return token != "" +} diff --git a/internal/api/middleware/auth_middleware_test.go b/internal/api/middleware/auth_middleware_test.go new file mode 100644 index 0000000..abdd703 --- /dev/null +++ b/internal/api/middleware/auth_middleware_test.go @@ -0,0 +1,7 @@ +package middleware_test + +import "testing" + +func TestAuthMiddlewareValidToken(t *testing.T) { + +} diff --git a/internal/config/config.go b/internal/config/config.go new file mode 100644 index 0000000..2c3ecc1 --- /dev/null +++ b/internal/config/config.go @@ -0,0 +1,50 @@ +package config + +import ( + "flag" + "log" + "os" + + "github.com/ilyakaznacheev/cleanenv" + "github.com/joho/godotenv" +) + +type HTTPServer struct { + Address string `yaml:"address" env-required:"true"` + Port int `yaml:"port" env-required:"true"` +} + +type Config struct { + Env string `yaml:"env" env-required:"true" env-default:"production` + DatabaseUri string `env:"DATABASE_URI" env-required:"true"` + HTTPServer `yaml:"http_server"` +} + +func ConfigMustLoad() *Config { + var configPath string + godotenv.Load(".env") + configPath = os.Getenv("CONFIG_PATH") + + if configPath == "" { + flags := flag.String("config", "", "path to config file") + flag.Parse() + + configPath = *flags + + if configPath == "" { + log.Fatal("config path not set") + } + } + + if _, err := os.Stat(configPath); os.IsNotExist(err) { + log.Fatalf("config file does not exist %s", configPath) + } + + var cfg Config + err := cleanenv.ReadConfig(configPath, &cfg) + if err != nil { + log.Fatalf("can not read config file %s", err.Error()) + } + + return &cfg +} diff --git a/internal/database/database.go b/internal/database/database.go new file mode 100644 index 0000000..5960cff --- /dev/null +++ b/internal/database/database.go @@ -0,0 +1,32 @@ +package database + +import ( + "database/sql" + "log" + + _ "github.com/lib/pq" +) + +var DB *sql.DB + +func InitDB(uri string) { + DB, err := sql.Open("postgres", uri) + if err != nil { + log.Fatalf("Error opening database: %v", err) + } + + err = DB.Ping() + if err != nil { + log.Fatalf("Error connecting to the database: %v", err) + } + + log.Println("Connected to the PostgreSQL database successfully") + +} + +func CloseDB() { + err := DB.Close() + if err != nil { + log.Printf("Error closing the database: %v", err) + } +} diff --git a/internal/models/user.go b/internal/models/user.go new file mode 100644 index 0000000..56657c2 --- /dev/null +++ b/internal/models/user.go @@ -0,0 +1,13 @@ +package models + +import "time" + +type User struct { + ID int `json:"id"` + Name string `json:"name" validate:"required"` + Username string `json:"username" validate:"required"` + Email string `json:"email" validate:"required,email"` + Password string `json:"-"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} diff --git a/internal/models/user_test.go b/internal/models/user_test.go new file mode 100644 index 0000000..cd35c6c --- /dev/null +++ b/internal/models/user_test.go @@ -0,0 +1,111 @@ +package models_test + +import ( + "testing" + "time" + + "github.com/gauravst/go-api-template/internal/models" + "github.com/go-playground/validator/v10" +) + +func TestUserValidation(t *testing.T) { + validate := validator.New() + + tests := []struct { + name string + user models.User + wantErr bool + }{ + { + name: "valid user", + user: models.User{ + Name: "John Doe", + Username: "johndoe", + Email: "john@example.com", + Password: "password123", + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + }, + wantErr: false, + }, + { + name: "missing name", + user: models.User{ + Username: "johndoe", + Email: "john@example.com", + Password: "password123", + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + }, + wantErr: true, + }, + { + name: "missing username", + user: models.User{ + Name: "John Doe", + Email: "john@example.com", + Password: "password123", + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + }, + wantErr: true, + }, + { + name: "invalid email", + user: models.User{ + Name: "John Doe", + Username: "johndoe", + Email: "invalid-email", + Password: "password123", + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + }, + wantErr: true, + }, + { + name: "missing email", + user: models.User{ + Name: "John Doe", + Username: "johndoe", + Password: "password123", + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + }, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := validate.Struct(tt.user) + if (err != nil) != tt.wantErr { + t.Errorf("User validation failed: %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestUserTimestamps(t *testing.T) { + user := models.User{ + Name: "John Doe", + Username: "johndoe", + Email: "john@example.com", + Password: "password123", + } + + if !user.CreatedAt.IsZero() || !user.UpdatedAt.IsZero() { + t.Error("expected CreatedAt and UpdatedAt to be zero values initially") + } + + now := time.Now() + user.CreatedAt = now + user.UpdatedAt = now + + if user.CreatedAt.IsZero() || user.UpdatedAt.IsZero() { + t.Error("expected CreatedAt and UpdatedAt to be set") + } + + if user.CreatedAt != now || user.UpdatedAt != now { + t.Error("expected CreatedAt and UpdatedAt to match the current time") + } +} diff --git a/internal/repositories/user_repository.go b/internal/repositories/user_repository.go new file mode 100644 index 0000000..632c925 --- /dev/null +++ b/internal/repositories/user_repository.go @@ -0,0 +1,72 @@ +package repositories + +import ( + "database/sql" + + "github.com/gauravst/go-api-template/internal/models" +) + +// UserRepository defines the interface for user-related database operations +type UserRepository interface { + CreateUser(user *models.User) error + GetUserByID(id int) (*models.User, error) + UpdateUser(user *models.User) error + DeleteUser(id int) error +} + +// userRepository implements the UserRepository interface +type userRepository struct { + db *sql.DB +} + +// NewUserRepository creates a new instance of userRepository +func NewUserRepository(db *sql.DB) UserRepository { + return &userRepository{ + db: db, + } +} + +// CreateUser inserts a new user into the database +func (r *userRepository) CreateUser(user *models.User) error { + query := `INSERT INTO users (name, email) VALUES ($1, $2) RETURNING id` + err := r.db.QueryRow(query, user.Name, user.Email).Scan(&user.ID) + if err != nil { + return err + } + + return nil +} + +// GetUserByID retrieves a user by their ID from the database +func (r *userRepository) GetUserByID(id int) (*models.User, error) { + user := &models.User{} + query := `SELECT id, name, email FROM users WHERE id = $1` + err := r.db.QueryRow(query, id).Scan(&user.ID, &user.Name, &user.Email) + if err != nil { + return nil, err + } + + return user, nil +} + +// UpdateUser updates an existing user in the database +func (r *userRepository) UpdateUser(user *models.User) error { + query := `UPDATE users SET name = $1, email = $2 WHERE id = $3` + _, err := r.db.Exec(query, user.Name, user.Email, user.ID) + if err != nil { + return err + } + + return nil +} + +// DeleteUser deletes a user by their ID from the database +func (r *userRepository) DeleteUser(id int) error { + query := `DELETE FROM users WHERE id = $1` + _, err := r.db.Exec(query, id) + if err != nil { + return err + } + + return nil +} diff --git a/internal/repositories/user_repository_test.go b/internal/repositories/user_repository_test.go new file mode 100644 index 0000000..253e610 --- /dev/null +++ b/internal/repositories/user_repository_test.go @@ -0,0 +1,19 @@ +package repositories_test + +import "testing" + +func TestCreateUser(t *testing.T) { + +} + +func TestUserGetUserById(t *testing.T) { + +} + +func TestUpdateUser(t *testing.T) { + +} + +func DeleteUser(t *testing.T) { + +} diff --git a/internal/services/user_service.go b/internal/services/user_service.go new file mode 100644 index 0000000..9a3d753 --- /dev/null +++ b/internal/services/user_service.go @@ -0,0 +1,73 @@ +package services + +import ( + "errors" + "fmt" + + "github.com/gauravst/go-api-template/internal/models" + "github.com/gauravst/go-api-template/internal/repositories" +) + +type UserService interface { + CreateUser(user models.User) error + GetUserByID(id int) (*models.User, error) + UpdateUser(user models.User) error + DeleteUser(id int) error +} + +type userService struct { + userRepo repositories.UserRepository +} + +func NewUserService(userRepo repositories.UserRepository) UserService { + return &userService{ + userRepo: userRepo, + } +} + +func (s *userService) CreateUser(user models.User) error { + if user.Name == "" { + return errors.New("user name cannot be empty") + } + + err := s.userRepo.CreateUser(&user) + if err != nil { + return fmt.Errorf("failed to create user: %w", err) + } + + return nil +} + +// GetUserByID retrieves a user by their ID +func (s *userService) GetUserByID(id int) (*models.User, error) { + user, err := s.userRepo.GetUserByID(id) + if err != nil { + return nil, fmt.Errorf("failed to get user: %w", err) + } + + return user, nil +} + +// UpdateUser updates an existing user +func (s *userService) UpdateUser(user models.User) error { + if user.Name == "" { + return errors.New("user name cannot be empty") + } + + err := s.userRepo.UpdateUser(&user) + if err != nil { + return fmt.Errorf("failed to update user: %w", err) + } + + return nil +} + +// DeleteUser deletes a user by their ID +func (s *userService) DeleteUser(id int) error { + err := s.userRepo.DeleteUser(id) + if err != nil { + return fmt.Errorf("failed to delete user: %w", err) + } + + return nil +} diff --git a/internal/services/user_service_test.go b/internal/services/user_service_test.go new file mode 100644 index 0000000..396fd0b --- /dev/null +++ b/internal/services/user_service_test.go @@ -0,0 +1,19 @@ +package services_test + +import "testing" + +func TestCreateUser(t *testing.T) { + +} + +func TestGetUserById(t *testing.T) { + +} + +func TestUpdateUser(t *testing.T) { + +} + +func TestDeleteUser(t *testing.T) { + +} diff --git a/internal/utils/response/response.go b/internal/utils/response/response.go new file mode 100644 index 0000000..3fd8877 --- /dev/null +++ b/internal/utils/response/response.go @@ -0,0 +1,53 @@ +package response + +import ( + "encoding/json" + "fmt" + "net/http" + "strings" + + "github.com/go-playground/validator/v10" +) + +type Response struct { + Status string `json:"status"` + Error string `json:"error"` +} + +const ( + StatusOK = "OK" + StatusError = "Error" +) + +func WriteJson(w http.ResponseWriter, status int, data interface{}) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(status) + + return json.NewEncoder(w).Encode(data) +} + +func GeneralError(err error) Response { + return Response{ + Status: StatusError, + Error: err.Error(), + } +} + +func ValidationError(errs validator.ValidationErrors) Response { + var errMsgs []string + + for _, err := range errs { + switch err.ActualTag() { + case "required": + errMsgs = append(errMsgs, fmt.Sprintf("field %s is required field", err.Field())) + + default: + errMsgs = append(errMsgs, fmt.Sprintf("field %s is invalid", err.Field())) + } + } + + return Response{ + Status: StatusError, + Error: strings.Join(errMsgs, ", "), + } +} diff --git a/makefile b/makefile new file mode 100644 index 0000000..90e00ef --- /dev/null +++ b/makefile @@ -0,0 +1,106 @@ +include .env +export + +# Variables +BINARY_NAME=go-api-template +GO_FILES=$(shell find . -name '*.go' -not -path './vendor/*') +MIGRATE_PATH=./migrations +DB_URL=$(DATABASE_URI) +APP_NAME=go-api-template +DOCKER_IMAGE_NAME=go-api-template +DOCKER_TAG=latest +PORT=8080 + +# Default target +all: build + +# Build the application +build: + @echo "Building the application..." + go build -o bin/$(BINARY_NAME) cmd/go-api-template/main.go + +# Run the application +run: + @echo "Running the application..." + go run cmd/go-api-template/main.go + +# Run tests +test: + @echo "Running tests..." + go test -v ./... + +# Format code +fmt: + @echo "Formatting code..." + go fmt ./... + +# Clean build artifacts +clean: + @echo "Cleaning up..." + rm -rf bin/ + +# Install dependencies +deps: + @echo "Installing dependencies..." + go mod tidy + +# Run all up migrations +migrate-up: + migrate -path $(MIGRATE_PATH) -database $(DB_URL) up + +# Run all down migrations +migrate-down: + migrate -path $(MIGRATE_PATH) -database $(DB_URL) down + +## Build the Docker image +docker-build: + docker build -t $(DOCKER_IMAGE_NAME):$(DOCKER_TAG) . + +## Run the Docker container +docker-run: + docker run -p $(PORT):$(PORT) $(DOCKER_IMAGE_NAME):$(DOCKER_TAG) + +## Stop the Docker container +docker-stop: + docker stop $$(docker ps -q --filter ancestor=$(DOCKER_IMAGE_NAME):$(DOCKER_TAG)) + +## Remove the Docker container +docker-rm: + docker rm $$(docker ps -a -q --filter ancestor=$(DOCKER_IMAGE_NAME):$(DOCKER_TAG)) + +## Remove the Docker image +docker-rmi: + docker rmi $(DOCKER_IMAGE_NAME):$(DOCKER_TAG) + +## Clean up Docker resources (stop, remove container, and remove image) +docker-clean: docker-stop docker-rm docker-rmi + +# Combined commands + +## Build and run the Go application +all: build run + +## Build and run the Docker container +docker-all: docker-build docker-run + +# Run all checks (format, test, build) +check: fmt test build + +# Help (list all targets) +help: + @echo "Available targets:" + @echo " build - Build the application" + @echo " run - Run the application" + @echo " test - Run tests" + @echo " fmt - Format code" + @echo " clean - Clean build artifacts" + @echo " deps - Install dependencies" + @echo " check - Run all checks (format, test, build)" + @echo " help - Show this help message" + @echo " docker-build - Build the Docker image" + @echo " docker-run - Run the Docker container" + @echo " docker-stop - Stop the Docker container" + @echo " docker-rm - Remove the Docker container" + @echo " docker-rmi - Remove the Docker image" + @echo " docker-clean - Clean up Docker resources (stop, remove container, and remove image)" + @echo " docker-all - Build and run the Docker container" diff --git a/migrations/001_initial_schema.up.sql b/migrations/001_initial_schema.up.sql new file mode 100644 index 0000000..cc4b813 --- /dev/null +++ b/migrations/001_initial_schema.up.sql @@ -0,0 +1,10 @@ +--- 001_initial_schema.up.sql +CREATE TABLE users ( + id SERIAL PRIMARY KEY, + name TEXT NOT NULL, + username TEXT NOT NULL, + email TEXT UNIQUE NOT NULL, + password TEXT NOT NULL, + createdAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updatedAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); diff --git a/test/e2e/db_test.go b/test/e2e/db_test.go new file mode 100644 index 0000000..e69de29 diff --git a/test/integration/user_integration_test.go b/test/integration/user_integration_test.go new file mode 100644 index 0000000..e69de29