first commit
This commit is contained in:
commit
66ec1ec60a
32
cmd/server/main.go
Normal file
32
cmd/server/main.go
Normal file
@ -0,0 +1,32 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"boruto/internal/app"
|
||||
"boruto/internal/config"
|
||||
)
|
||||
|
||||
func main() {
|
||||
cfg, err := config.Load()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
handler := app.NewRouter(cfg)
|
||||
|
||||
server := &http.Server{
|
||||
Addr: cfg.HTTP.Addr,
|
||||
Handler: handler,
|
||||
ReadTimeout: cfg.HTTP.ReadTimeout,
|
||||
WriteTimeout: cfg.HTTP.WriteTimeout,
|
||||
IdleTimeout: cfg.HTTP.IdleTimeout,
|
||||
}
|
||||
|
||||
log.Printf("starting server on %s in %s mode", cfg.HTTP.Addr, cfg.AppEnv)
|
||||
|
||||
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
5
go.mod
Normal file
5
go.mod
Normal file
@ -0,0 +1,5 @@
|
||||
module boruto
|
||||
|
||||
go 1.24.1
|
||||
|
||||
require github.com/go-chi/chi/v5 v5.3.0
|
||||
2
go.sum
Normal file
2
go.sum
Normal file
@ -0,0 +1,2 @@
|
||||
github.com/go-chi/chi/v5 v5.3.0 h1:halUjDxhshgXHMrao5bB8eNBXo/rnzwr8m5m36glehM=
|
||||
github.com/go-chi/chi/v5 v5.3.0/go.mod h1:R+tYY2hNuVUUjxoPtqUdgBqevM9s9njzkTLutVsOCto=
|
||||
29
internal/app/routes.go
Normal file
29
internal/app/routes.go
Normal file
@ -0,0 +1,29 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/chi/v5/middleware"
|
||||
|
||||
"boruto/internal/config"
|
||||
"boruto/internal/http/handlers"
|
||||
)
|
||||
|
||||
func NewRouter(cfg config.Config) http.Handler {
|
||||
r := chi.NewRouter()
|
||||
|
||||
r.Use(middleware.RequestID)
|
||||
r.Use(middleware.RealIP)
|
||||
r.Use(middleware.Logger)
|
||||
r.Use(middleware.Recoverer)
|
||||
|
||||
r.Get("/health", handlers.Health)
|
||||
|
||||
r.Route("/users", func(r chi.Router) {
|
||||
r.Get("/", handlers.ListUsers)
|
||||
r.Post("/", handlers.CreateUser)
|
||||
})
|
||||
|
||||
return r
|
||||
}
|
||||
127
internal/config/config.go
Normal file
127
internal/config/config.go
Normal file
@ -0,0 +1,127 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
AppEnv string
|
||||
|
||||
HTTP HTTPConfig
|
||||
|
||||
Database DatabaseConfig
|
||||
}
|
||||
|
||||
type HTTPConfig struct {
|
||||
Addr string
|
||||
ReadTimeout time.Duration
|
||||
WriteTimeout time.Duration
|
||||
IdleTimeout time.Duration
|
||||
}
|
||||
|
||||
type DatabaseConfig struct {
|
||||
URL string
|
||||
MaxOpenConns int
|
||||
MaxIdleConns int
|
||||
ConnMaxLifetime time.Duration
|
||||
}
|
||||
|
||||
func Load() (Config, error) {
|
||||
cfg := Config{
|
||||
AppEnv: getString("APP_ENV", "development"),
|
||||
|
||||
HTTP: HTTPConfig{
|
||||
Addr: getString("HTTP_ADDR", ":8080"),
|
||||
ReadTimeout: getDuration("HTTP_READ_TIMEOUT", 5*time.Second),
|
||||
WriteTimeout: getDuration("HTTP_WRITE_TIMEOUT", 10*time.Second),
|
||||
IdleTimeout: getDuration("HTTP_IDLE_TIMEOUT", 60*time.Second),
|
||||
},
|
||||
|
||||
Database: DatabaseConfig{
|
||||
URL: getString("DATABASE_URL", ""),
|
||||
MaxOpenConns: getInt("DB_MAX_OPEN_CONNS", 25),
|
||||
MaxIdleConns: getInt("DB_MAX_IDLE_CONNS", 25),
|
||||
ConnMaxLifetime: getDuration("DB_CONN_MAX_LIFETIME", 5*time.Minute),
|
||||
},
|
||||
}
|
||||
|
||||
if err := cfg.Validate(); err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func (c Config) Validate() error {
|
||||
if c.AppEnv == "" {
|
||||
return fmt.Errorf("APP_ENV is required")
|
||||
}
|
||||
|
||||
switch c.AppEnv {
|
||||
case "development", "test", "staging", "production":
|
||||
default:
|
||||
return fmt.Errorf("APP_ENV must be one of: development, test, staging, production")
|
||||
}
|
||||
|
||||
if c.HTTP.Addr == "" {
|
||||
return fmt.Errorf("HTTP_ADDR is required")
|
||||
}
|
||||
|
||||
if c.Database.URL == "" && c.AppEnv != "test" {
|
||||
return fmt.Errorf("DATABASE_URL is required")
|
||||
}
|
||||
|
||||
if c.Database.MaxOpenConns < 1 {
|
||||
return fmt.Errorf("DB_MAX_OPEN_CONNS must be at least 1")
|
||||
}
|
||||
|
||||
if c.Database.MaxIdleConns < 1 {
|
||||
return fmt.Errorf("DB_MAX_IDLE_CONNS must be at least 1")
|
||||
}
|
||||
|
||||
if c.Database.MaxIdleConns > c.Database.MaxOpenConns {
|
||||
return fmt.Errorf("DB_MAX_IDLE_CONNS cannot exceed DB_MAX_OPEN_CONNS")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getString(key, fallback string) string {
|
||||
value, ok := os.LookupEnv(key)
|
||||
if !ok || value == "" {
|
||||
return fallback
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
func getInt(key string, fallback int) int {
|
||||
value, ok := os.LookupEnv(key)
|
||||
if !ok || value == "" {
|
||||
return fallback
|
||||
}
|
||||
|
||||
parsed, err := strconv.Atoi(value)
|
||||
if err != nil {
|
||||
return fallback
|
||||
}
|
||||
|
||||
return parsed
|
||||
}
|
||||
|
||||
func getDuration(key string, fallback time.Duration) time.Duration {
|
||||
value, ok := os.LookupEnv(key)
|
||||
if !ok || value == "" {
|
||||
return fallback
|
||||
}
|
||||
|
||||
parsed, err := time.ParseDuration(value)
|
||||
if err != nil {
|
||||
return fallback
|
||||
}
|
||||
|
||||
return parsed
|
||||
}
|
||||
29
internal/http/handlers/handlers.go
Normal file
29
internal/http/handlers/handlers.go
Normal file
@ -0,0 +1,29 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type response map[string]any
|
||||
|
||||
func Health(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, http.StatusOK, response{"status": "ok"})
|
||||
}
|
||||
|
||||
func ListUsers(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, http.StatusOK, response{"users": []response{}})
|
||||
}
|
||||
|
||||
func CreateUser(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, http.StatusCreated, response{"status": "created"})
|
||||
}
|
||||
|
||||
func writeJSON(w http.ResponseWriter, status int, payload any) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(status)
|
||||
|
||||
if err := json.NewEncoder(w).Encode(payload); err != nil {
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user