From go-utils
Initialize go web server project with best practice layout, including logging and proper server configuration
How this skill is triggered — by the user, by Claude, or both
Slash command
/go-utils:init-server-projectThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Scaffold a production-ready Go web server project using standard library `net/http`, `log/slog` structured logging, templ templating, and graceful shutdown.
Scaffold a production-ready Go web server project using standard library net/http, log/slog structured logging, templ templating, and graceful shutdown.
Project name: $ARGUMENTS
Ask the user for any missing information. You need:
github.com/user/projectname. Ask the user.Create the project directory. If a relative path, create it relative to the current working directory.
mkdir -p <project>/cmd/server
mkdir -p <project>/internal/server/templates/layouts
mkdir -p <project>/internal/server/templates/pages
If rate limiting is requested:
mkdir -p <project>/internal/server/limiter
cmd/server/main.goIf rate limiting is included:
package main
import (
"flag"
"fmt"
"log/slog"
"os"
"time"
"<MODULE_PATH>/internal/server"
)
func main() {
addr := flag.String("addr", "127.0.0.1:8080", "listen address")
flag.Parse()
logger := slog.New(slog.NewTextHandler(
os.Stderr,
&slog.HandlerOptions{
Level: slog.LevelInfo,
},
))
config := server.ServerConfig{
Logger: logger,
Addr: *addr,
RateLimitRefillRate: 1 * time.Second,
RateLimitBucketSize: 10,
RateLimitMaxIdle: 10 * time.Minute,
RateLimitCleanupInterval: time.Minute,
}
if err := server.Run(config); err != nil {
fmt.Fprintf(os.Stderr, "Server exit with error: %s", err.Error())
}
}
If rate limiting is NOT included, remove the time import and the RateLimit* config fields:
package main
import (
"flag"
"fmt"
"log/slog"
"os"
"<MODULE_PATH>/internal/server"
)
func main() {
addr := flag.String("addr", "127.0.0.1:8080", "listen address")
flag.Parse()
logger := slog.New(slog.NewTextHandler(
os.Stderr,
&slog.HandlerOptions{
Level: slog.LevelInfo,
},
))
config := server.ServerConfig{
Logger: logger,
Addr: *addr,
}
if err := server.Run(config); err != nil {
fmt.Fprintf(os.Stderr, "Server exit with error: %s", err.Error())
}
}
internal/server/server.goIf rate limiting is included:
package server
import (
"context"
"log/slog"
"net/http"
"os"
"os/signal"
"time"
"<MODULE_PATH>/internal/server/limiter"
"<MODULE_PATH>/internal/server/templates/pages"
)
type ServerConfig struct {
Logger *slog.Logger
// [host]:[port]
Addr string
RateLimitRefillRate time.Duration
RateLimitBucketSize int
RateLimitMaxIdle time.Duration
RateLimitCleanupInterval time.Duration
}
func Run(config ServerConfig) error {
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, os.Kill)
defer stop()
srv := newServer(ctx, &config)
httpServer := &http.Server{
Addr: config.Addr,
Handler: srv,
}
errCh := make(chan error)
go func() {
slog.Info("Server listening", "address", config.Addr)
if err := httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
errCh <- err
}
}()
select {
case err := <-errCh:
return err
case <-ctx.Done():
shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
return httpServer.Shutdown(shutdownCtx)
}
}
func newServer(ctx context.Context, cfg *ServerConfig) http.Handler {
mux := http.NewServeMux()
addRoutes(mux, cfg)
rateLimiter := limiter.NewLimiter[string](
ctx,
cfg.RateLimitRefillRate,
cfg.RateLimitBucketSize,
cfg.RateLimitMaxIdle,
cfg.RateLimitCleanupInterval,
)
var handler http.Handler = mux
handler = RateLimitMiddleware(ctx, rateLimiter)(handler)
handler = LoggingMiddleware(cfg.Logger)(handler)
return handler
}
func addRoutes(mux *http.ServeMux, deps *ServerConfig) {
mux.Handle("GET /", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_ = pages.Index().Render(r.Context(), w)
}))
}
If rate limiting is NOT included:
package server
import (
"context"
"log/slog"
"net/http"
"os"
"os/signal"
"time"
"<MODULE_PATH>/internal/server/templates/pages"
)
type ServerConfig struct {
Logger *slog.Logger
// [host]:[port]
Addr string
}
func Run(config ServerConfig) error {
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, os.Kill)
defer stop()
srv := newServer(ctx, &config)
httpServer := &http.Server{
Addr: config.Addr,
Handler: srv,
}
errCh := make(chan error)
go func() {
slog.Info("Server listening", "address", config.Addr)
if err := httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
errCh <- err
}
}()
select {
case err := <-errCh:
return err
case <-ctx.Done():
shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
return httpServer.Shutdown(shutdownCtx)
}
}
func newServer(_ context.Context, cfg *ServerConfig) http.Handler {
mux := http.NewServeMux()
addRoutes(mux, cfg)
var handler http.Handler = mux
handler = LoggingMiddleware(cfg.Logger)(handler)
return handler
}
func addRoutes(mux *http.ServeMux, deps *ServerConfig) {
mux.Handle("GET /", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_ = pages.Index().Render(r.Context(), w)
}))
}
internal/server/middleware.goIf rate limiting is included:
package server
import (
"context"
"log/slog"
"net/http"
"time"
"<MODULE_PATH>/internal/server/limiter"
)
type statusRecorder struct {
http.ResponseWriter
status int
}
func (r *statusRecorder) WriteHeader(code int) {
r.status = code
r.ResponseWriter.WriteHeader(code)
}
func LoggingMiddleware(logger *slog.Logger) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
rec := &statusRecorder{ResponseWriter: w, status: http.StatusOK}
next.ServeHTTP(rec, r)
logger.Info("request",
"method", r.Method,
"path", r.URL.Path,
"remote_addr", r.RemoteAddr,
"status", rec.status,
"duration", time.Since(start),
)
})
}
}
func RateLimitMiddleware(ctx context.Context, l *limiter.Limiter[string]) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !l.Allow(ctx, r.RemoteAddr) {
http.Error(w, "Too Many Requests", http.StatusTooManyRequests)
return
}
next.ServeHTTP(w, r)
})
}
}
If rate limiting is NOT included, omit the context import, the limiter import, and RateLimitMiddleware:
package server
import (
"log/slog"
"net/http"
"time"
)
type statusRecorder struct {
http.ResponseWriter
status int
}
func (r *statusRecorder) WriteHeader(code int) {
r.status = code
r.ResponseWriter.WriteHeader(code)
}
func LoggingMiddleware(logger *slog.Logger) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
rec := &statusRecorder{ResponseWriter: w, status: http.StatusOK}
next.ServeHTTP(rec, r)
logger.Info("request",
"method", r.Method,
"path", r.URL.Path,
"remote_addr", r.RemoteAddr,
"status", rec.status,
"duration", time.Since(start),
)
})
}
}
internal/server/limiter/limiter.go (only if rate limiting requested)package limiter
import (
"context"
"sync"
"time"
)
// Bucket implements a token bucket rate limiter. Tokens are added at a fixed
// rate and consumers take tokens to perform actions. When empty, actions are denied.
type Bucket struct {
bucket chan struct{}
refillRate time.Duration
bucketSize int
}
// Add one token to the bucket every refillRate
func (b *Bucket) startRefill(ctx context.Context) {
for {
select {
case <-time.After(b.refillRate):
b.bucket <- struct{}{}
case <-ctx.Done():
return
}
}
}
// TryGetToken attempts to take a token from the bucket.
// Returns true if a token was available, false otherwise. Non-blocking.
func (b *Bucket) TryGetToken() bool {
select {
case <-b.bucket:
return true
default:
return false
}
}
// NewBucket creates a token bucket that refills one token every refillRate,
// up to bucketSize tokens. The bucket starts full. The refill goroutine stops
// when ctx is cancelled.
func NewBucket(ctx context.Context, refillRate time.Duration, bucketSize int) *Bucket {
b := &Bucket{
bucket: make(chan struct{}, bucketSize),
refillRate: refillRate,
bucketSize: bucketSize,
}
for range bucketSize {
b.bucket <- struct{}{}
}
go b.startRefill(ctx)
return b
}
type entry struct {
bucket *Bucket
lastSeen time.Time
}
// Limiter manages per-key rate limiting using token buckets.
// Each unique key gets its own bucket. Stale buckets are automatically cleaned up.
type Limiter[T comparable] struct {
mu sync.Mutex
buckets map[T]*entry
refillRate time.Duration
bucketSize int
}
// NewLimiter creates a rate limiter that manages per-key token buckets.
// Each bucket refills one token every refillRate, up to bucketSize tokens.
// Buckets idle for longer than maxIdle are removed every cleanupInterval.
// All goroutines stop when ctx is cancelled.
func NewLimiter[T comparable](ctx context.Context, refillRate time.Duration, bucketSize int, maxIdle, cleanupInterval time.Duration) *Limiter[T] {
l := &Limiter[T]{
buckets: make(map[T]*entry),
refillRate: refillRate,
bucketSize: bucketSize,
}
go l.cleanup(ctx, maxIdle, cleanupInterval)
return l
}
// GetBucket returns the bucket for the given key, creating one if it doesn't exist.
// Updates the last-seen time to prevent cleanup.
func (l *Limiter[T]) GetBucket(ctx context.Context, v T) *Bucket {
l.mu.Lock()
defer l.mu.Unlock()
e, exists := l.buckets[v]
if exists {
e.lastSeen = time.Now()
return e.bucket
}
b := NewBucket(ctx, l.refillRate, l.bucketSize)
l.buckets[v] = &entry{
bucket: b,
lastSeen: time.Now(),
}
return b
}
// Allow checks if an action is allowed for the given key.
// Returns true if a token was available, false if rate limited.
func (l *Limiter[T]) Allow(ctx context.Context, v T) bool {
return l.GetBucket(ctx, v).TryGetToken()
}
func (l *Limiter[T]) cleanup(ctx context.Context, maxIdle, interval time.Duration) {
for {
select {
case <-time.After(interval):
l.mu.Lock()
now := time.Now()
for k, e := range l.buckets {
if now.Sub(e.lastSeen) > maxIdle {
delete(l.buckets, k)
}
}
l.mu.Unlock()
case <-ctx.Done():
return
}
}
}
internal/server/templates/layouts/base.templReplace <PROJECT_TITLE> with the project name in title case.
package layouts
templ Base(title string) {
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>{ title }</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://unpkg.com/[email protected]"></script>
</head>
<body class="bg-gray-900 text-gray-100 font-sans min-h-screen" hx-boost="true">
{ children... }
</body>
</html>
}
internal/server/templates/pages/index.templpackage pages
import "<MODULE_PATH>/internal/server/templates/layouts"
templ Index() {
@layouts.Base("<PROJECT_TITLE>") {
<div class="max-w-4xl mx-auto px-6 py-16">
<header class="mb-12">
<h1 class="text-4xl font-bold text-emerald-400 tracking-tight"><PROJECT_TITLE></h1>
<p class="mt-3 text-lg text-gray-400">Welcome to your new server.</p>
</header>
</div>
}
}
go.modmodule <MODULE_PATH>
go 1.24
tool github.com/a-h/templ/cmd/templ
require github.com/a-h/templ v0.3.977
Makefiletempl:
go tool templ generate
server: templ
go build -o ./bin/server ./cmd/server
run: templ
go run ./cmd/server -addr 127.0.0.1:8080
.gitignorebin/
.claude/
.claude-sessions/
Run the following commands inside the project directory. The templ files must be generated first so that go mod tidy can resolve the imports from the generated _templ.go files.
cd <project> && go tool templ generate && go mod tidy
<MODULE_PATH>, <PROJECT_TITLE>, and <project> must be replaced with the actual values from user input.internal/server/limiter/limiter.go and rate-limit-related code if the user requests it.github.com/a-h/templ. No routers, no frameworks.go tool templ generate (not go run github.com/a-h/templ/cmd/templ). Use range-over-int (for range n).go tool templ generate. Only create .templ source files.cd <project> && go mod tidy && make server to confirm the project compiles.npx claudepluginhub shreda/skills --plugin go-utilsProvides idiomatic Go patterns for backend APIs with Gin, Echo, Fiber: standard project structure, custom error handling, handler dependency injection, concurrency best practices.
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.
Scaffolds boilerplate for APIs (FastAPI, Express), web apps (Next.js, Nuxt, SvelteKit), CLI tools, libraries, monorepos with best-practice stacks and directory structures.