first commit

This commit is contained in:
Maxime Duchene-Savard 2026-06-19 03:23:44 -04:00
commit 66ec1ec60a
6 changed files with 224 additions and 0 deletions

32
cmd/server/main.go Normal file
View 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
View 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
View 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
View 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
View 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
}

View 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)
}
}