From beagle-go
Provides Go web server architecture with net/http 1.22+ routing, project structure patterns, graceful shutdown, and dependency injection. Use for building Go apps, layouts, and dependencies.
npx claudepluginhub existential-birds/beagle --plugin beagle-goThis skill uses the workspace's default tool permissions.
| Topic | Reference |
Generates design tokens/docs from CSS/Tailwind/styled-components codebases, audits visual consistency across 10 dimensions, detects AI slop in UI.
Records polished WebM UI demo videos of web apps using Playwright with cursor overlay, natural pacing, and three-phase scripting. Activates for demo, walkthrough, screen recording, or tutorial requests.
Delivers idiomatic Kotlin patterns for null safety, immutability, sealed classes, coroutines, Flows, extensions, DSL builders, and Gradle DSL. Use when writing, reviewing, refactoring, or designing Kotlin code.
| Topic | Reference |
|---|---|
| Flat vs modular project layout, migration signals | references/project-structure.md |
| Graceful shutdown with signal handling | references/graceful-shutdown.md |
| Dependency injection patterns, testing seams | references/dependency-injection.md |
net/http and the Go 1.22+ enhanced ServeMux for routing. Only reach for a framework (chi, echo, gin) when you have a concrete need the stdlib cannot satisfy (e.g., complex middleware chains, regex routes).var.init() side effects, no framework auto-wiring. main.go is the composition root where everything is assembled visibly.Go 1.22 upgraded http.ServeMux with method-based routing and path parameters, eliminating the most common reason for third-party routers.
mux := http.NewServeMux()
mux.HandleFunc("GET /api/users", s.handleListUsers)
mux.HandleFunc("GET /api/users/{id}", s.handleGetUser)
mux.HandleFunc("POST /api/users", s.handleCreateUser)
mux.HandleFunc("DELETE /api/users/{id}", s.handleDeleteUser)
func (s *Server) handleGetUser(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
if id == "" {
http.Error(w, "missing id", http.StatusBadRequest)
return
}
user, err := s.users.GetUser(r.Context(), id)
if err != nil {
s.logger.Error("getting user", "err", err, "id", id)
http.Error(w, "internal error", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(user)
}
// Exact match on trailing slash -- serves /api/files/ only
mux.HandleFunc("GET /api/files/", s.handleListFiles)
// Wildcard to end of path -- /api/files/path/to/doc.txt
mux.HandleFunc("GET /api/files/{path...}", s.handleGetFile)
The new ServeMux uses most-specific-wins precedence:
GET /api/users/{id} is more specific than GET /api/users/GET /api/users/me is more specific than GET /api/users/{id}The Server struct is the central dependency container for your application. It holds all shared dependencies and implements http.Handler.
type Server struct {
db *sql.DB
logger *slog.Logger
router *http.ServeMux
}
func NewServer(db *sql.DB, logger *slog.Logger) *Server {
s := &Server{
db: db,
logger: logger,
router: http.NewServeMux(),
}
s.routes()
return s
}
func (s *Server) routes() {
s.router.HandleFunc("GET /api/users/{id}", s.handleGetUser)
s.router.HandleFunc("POST /api/users", s.handleCreateUser)
s.router.HandleFunc("GET /healthz", s.handleHealth)
}
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
s.router.ServeHTTP(w, r)
}
Apply middleware at the http.Server level or per-route:
// Wrap entire server
httpServer := &http.Server{
Addr: ":8080",
Handler: requestLogger(s),
}
// Or per-route
s.router.Handle("GET /api/admin/", adminOnly(http.HandlerFunc(s.handleAdmin)))
func requestLogger(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
next.ServeHTTP(w, r)
slog.Info("request", "method", r.Method, "path", r.URL.Path, "dur", time.Since(start))
})
}
Choose based on project size:
cmd/, internal/ with domain packages. For larger apps with multiple bounded contexts. See references/project-structure.md.Start flat. Migrate when you see the signs described in the reference.
Every production Go server needs graceful shutdown. The pattern uses signal.NotifyContext to listen for OS signals and http.Server.Shutdown to drain connections.
ctx, cancel := signal.NotifyContext(ctx, os.Interrupt, syscall.SIGTERM)
defer cancel()
// ... start server in goroutine ...
<-ctx.Done()
shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 10*time.Second)
defer shutdownCancel()
httpServer.Shutdown(shutdownCtx)
Full pattern with cleanup ordering in references/graceful-shutdown.md.
Load project-structure.md when:
Load graceful-shutdown.md when:
Load dependency-injection.md when:
// BAD -- untestable, hidden dependency
var db *sql.DB
func handleGetUser(w http.ResponseWriter, r *http.Request) {
db.QueryRow(...)
}
Pass db through a Server or Service struct instead.
Do not start with gin.Default() or echo.New(). Start with http.NewServeMux(). Only introduce a framework if you hit a real limitation of the stdlib that justifies the dependency.
A single handlers package with 50 files is not organization. Group by domain (user, order, billing), not by technical layer.
// BAD -- invisible side effects, untestable
func init() {
db, _ = sql.Open("postgres", os.Getenv("DATABASE_URL"))
}
All initialization belongs in main() or a run() function so it can be tested and errors can be handled.
// BAD -- couples handler to environment
func (s *Server) handleSendEmail(w http.ResponseWriter, r *http.Request) {
apiKey := os.Getenv("SENDGRID_API_KEY") // don't do this
}
Inject configuration values or clients through constructors.