From 5325e04a30d54eccec814ee82e6f0791f90eda82 Mon Sep 17 00:00:00 2001 From: Robin Dittmar Date: Fri, 3 Jul 2026 11:39:58 +0200 Subject: [PATCH] Initialized proper project structure --- cmd/api-server/main.go | 84 +++++++++++----------------- internal/api/handler/default.go | 44 +++++++++++++++ internal/api/handler/health.go | 18 ++++++ internal/api/middleware/telemetry.go | 10 ++++ internal/{ => api/request}/.gitkeep | 0 internal/api/response/json.go | 33 +++++++++++ internal/api/router/router.go | 22 ++++++++ internal/domain/.gitkeep | 0 internal/telemetry/.gitkeep | 0 9 files changed, 159 insertions(+), 52 deletions(-) create mode 100644 internal/api/handler/default.go create mode 100644 internal/api/handler/health.go create mode 100644 internal/api/middleware/telemetry.go rename internal/{ => api/request}/.gitkeep (100%) create mode 100644 internal/api/response/json.go create mode 100644 internal/api/router/router.go create mode 100644 internal/domain/.gitkeep create mode 100644 internal/telemetry/.gitkeep diff --git a/cmd/api-server/main.go b/cmd/api-server/main.go index ec435f3..a3c3449 100644 --- a/cmd/api-server/main.go +++ b/cmd/api-server/main.go @@ -1,67 +1,47 @@ package main import ( - "encoding/json" - "fmt" + "context" + "errors" "log" "net/http" + "os" + "os/signal" + "syscall" + "time" + + "github.com/robindittmar/dttmr-api/internal/api/router" ) -type ApiResponse struct { - Method string `json:"method"` - Url string `json:"url"` - Proto string `json:"proto"` - Header map[string]string `json:"header"` - Host string `json:"host"` - RemoteAddr string `json:"remoteAddr"` - Form map[string]string `json:"form"` -} - -type HealthResponse struct { - Status string `json:"status"` -} - func main() { - http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - var resp ApiResponse + cfg := router.Config{} + mux := router.NewMux(cfg) - resp.Method = r.Method - resp.Url = r.URL.String() - resp.Proto = r.Proto - resp.Header = make(map[string]string) - resp.Host = r.Host - resp.RemoteAddr = r.RemoteAddr - resp.Form = make(map[string]string) + srv := &http.Server{ + Addr: ":8080", + Handler: mux, + ReadTimeout: 5 * time.Second, + WriteTimeout: 10 * time.Second, + IdleTimeout: 120 * time.Second, + } - for k, v := range r.Header { - resp.Header[k] = v[0] + go func() { + log.Printf("Listening on %s\n", srv.Addr) + if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { + log.Fatalf("Failed to start server: %v", err) } + }() - if err := r.ParseForm(); err == nil { - for k, v := range r.Form { - resp.Form[k] = v[0] - } - } else { - log.Print(err) - } + quit := make(chan os.Signal, 1) + signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) + <-quit + log.Println("Shutting down server...") - err := json.NewEncoder(w).Encode(resp) - if err != nil { - log.Println(err) - return - } - }) + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() - http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) { - resp := HealthResponse{Status: "ok"} - - err := json.NewEncoder(w).Encode(resp) - if err != nil { - log.Println(err) - return - } - }) - - fmt.Println("Listening on port 8080") - log.Fatal(http.ListenAndServe(":8080", nil)) + if err := srv.Shutdown(ctx); err != nil { + log.Fatalf("Server forced to shutdown: %v", err) + } + log.Println("Server shutdown successful!") } diff --git a/internal/api/handler/default.go b/internal/api/handler/default.go new file mode 100644 index 0000000..43dc8e9 --- /dev/null +++ b/internal/api/handler/default.go @@ -0,0 +1,44 @@ +package handler + +import ( + "log" + "net/http" + + "github.com/robindittmar/dttmr-api/internal/api/response" +) + +type apiResponse struct { + Method string `json:"method"` + Url string `json:"url"` + Proto string `json:"proto"` + Header map[string]string `json:"header"` + Host string `json:"host"` + RemoteAddr string `json:"remoteAddr"` + Form map[string]string `json:"form"` +} + +func DefaultHandler(w http.ResponseWriter, r *http.Request) { + var resp apiResponse + + resp.Method = r.Method + resp.Url = r.URL.String() + resp.Proto = r.Proto + resp.Header = make(map[string]string) + resp.Host = r.Host + resp.RemoteAddr = r.RemoteAddr + resp.Form = make(map[string]string) + + for k, v := range r.Header { + resp.Header[k] = v[0] + } + + if err := r.ParseForm(); err == nil { + for k, v := range r.Form { + resp.Form[k] = v[0] + } + } else { + log.Println(err) + } + + response.JSON(w, http.StatusOK, resp) +} diff --git a/internal/api/handler/health.go b/internal/api/handler/health.go new file mode 100644 index 0000000..685f9c2 --- /dev/null +++ b/internal/api/handler/health.go @@ -0,0 +1,18 @@ +package handler + +import ( + "net/http" + + "github.com/robindittmar/dttmr-api/internal/api/response" +) + +type healthResponse struct { + Status string `json:"status"` +} + +func HealthHandler(w http.ResponseWriter, r *http.Request) { + resp := healthResponse{ + Status: "OK", + } + response.JSON(w, http.StatusOK, resp) +} diff --git a/internal/api/middleware/telemetry.go b/internal/api/middleware/telemetry.go new file mode 100644 index 0000000..41859c0 --- /dev/null +++ b/internal/api/middleware/telemetry.go @@ -0,0 +1,10 @@ +package middleware + +import ( + "net/http" +) + +func WithTelemetry(next http.Handler) http.Handler { + //return otelhttp.NewHandler(next, "dttmr-api") + return next +} diff --git a/internal/.gitkeep b/internal/api/request/.gitkeep similarity index 100% rename from internal/.gitkeep rename to internal/api/request/.gitkeep diff --git a/internal/api/response/json.go b/internal/api/response/json.go new file mode 100644 index 0000000..43ba2db --- /dev/null +++ b/internal/api/response/json.go @@ -0,0 +1,33 @@ +package response + +import ( + "encoding/json" + "log" + "net/http" +) + +func JSON(w http.ResponseWriter, status int, data any) { + payload, err := json.Marshal(data) + if err != nil { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusInternalServerError) + _, err := w.Write([]byte(`{"error": "internal server error: failed to marshal response"}`)) + if err != nil { + log.Println(err) + return + } + return + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(status) + _, err = w.Write(payload) + if err != nil { + log.Println(err) + return + } +} + +func Error(w http.ResponseWriter, status int, message string) { + JSON(w, status, map[string]string{"error": message}) +} diff --git a/internal/api/router/router.go b/internal/api/router/router.go new file mode 100644 index 0000000..f4586b5 --- /dev/null +++ b/internal/api/router/router.go @@ -0,0 +1,22 @@ +package router + +import ( + "net/http" + + "github.com/robindittmar/dttmr-api/internal/api/handler" + "github.com/robindittmar/dttmr-api/internal/api/middleware" +) + +type Config struct{} + +func NewMux(cfg Config) http.Handler { + mux := http.NewServeMux() + + mux.HandleFunc("/", handler.DefaultHandler) + mux.HandleFunc("GET /health", handler.HealthHandler) + + var httpHandler http.Handler = mux + httpHandler = middleware.WithTelemetry(httpHandler) + + return httpHandler +} diff --git a/internal/domain/.gitkeep b/internal/domain/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/internal/telemetry/.gitkeep b/internal/telemetry/.gitkeep new file mode 100644 index 0000000..e69de29