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