HWLab
Backend is running. UI coming in Phase 3.
```
4. Create `internal/api/handlers/health.go`:
```go
package handlers
import (
"encoding/json"
"net/http"
)
type HealthResponse struct {
Status string `json:"status"`
Version string `json:"version"`
}
func Health(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(HealthResponse{
Status: "ok",
Version: "0.1.0",
})
}
```
5. Create `internal/api/router.go` — chi router with go:embed SPA fallback. The embed directive MUST be in the same file as the var declaration:
```go
package api
import (
"embed"
"io/fs"
"net/http"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
"git.georgsen.dk/hwlab/internal/api/handlers"
)
//go:embed ../../web/dist
var staticFiles embed.FS
func NewRouter() http.Handler {
r := chi.NewRouter()
r.Use(middleware.Logger)
r.Use(middleware.Recoverer)
r.Use(middleware.RealIP)
r.Route("/api", func(r chi.Router) {
r.Get("/health", handlers.Health)
})
// SPA fallback — serve index.html for all non-API routes
staticFS, _ := fs.Sub(staticFiles, "web/dist")
fileServer := http.FileServer(http.FS(staticFS))
r.Handle("/*", fileServer)
return r
}
```
NOTE: The go:embed path `../../web/dist` is relative to the file location `internal/api/router.go`. Verify path resolves correctly. If the embed directive causes issues due to directory depth, move the embed var to `cmd/hwlab/main.go` and pass the fs.FS into NewRouter as a parameter instead.
6. Create `cmd/hwlab/main.go` — wire-up only, no business logic:
```go
package main
import (
"fmt"
"log"
"net/http"
"git.georgsen.dk/hwlab/internal/api"
"git.georgsen.dk/hwlab/internal/config"
)
func main() {
cfg, err := config.Load()
if err != nil {
log.Fatalf("config: %v", err)
}
router := api.NewRouter()
addr := fmt.Sprintf("%s:%d", cfg.Host, cfg.Port)
log.Printf("HWLab starting on %s", addr)
if err := http.ListenAndServe(addr, router); err != nil {
log.Fatalf("server: %v", err)
}
}
```
7. Write test `internal/api/handlers/health_test.go`:
```go
package handlers_test
import (
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"git.georgsen.dk/hwlab/internal/api/handlers"
)
func TestHealth(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/api/health", nil)
w := httptest.NewRecorder()
handlers.Health(w, req)
if w.Code != http.StatusOK {
t.Fatalf("expected 200, got %d", w.Code)
}
var resp handlers.HealthResponse
if err := json.NewDecoder(w.Body).Decode(&resp); err != nil {
t.Fatalf("decode: %v", err)
}
if resp.Status != "ok" {
t.Errorf("expected status=ok, got %s", resp.Status)
}
if resp.Version != "0.1.0" {
t.Errorf("expected version=0.1.0, got %s", resp.Version)
}
}
```