Zero dependency, lightweight HTTP client with a struct based config.
A tiny wrapper around chi so that module consumers wouldn't need to import chi directly.
It's unlikely to ever need to switch routers, but years ago I thought the same thing about Gin and now migrating to chi.
Each with their own go.mod, but both here for convenience.
github.com/toaweme/http- the HTTP client. Pure stdlib, no third-party dependencies. Go 1.19+.github.com/toaweme/http/server- the HTTP server: router, middleware, auth, JSON helpers. Depends only ongo-chi/chi.github.com/toaweme/http/server/sse- the Server-Sent Events writer and broadcast hub used by the server.
go get github.com/toaweme/http # client
go get github.com/toaweme/http/server # serverThe package is named
http, so import it as plainhttp. It only collides with stdlibnet/httpin a file that imports both - alias it (thttp) only there.
Build a Client from a Config, then call one method per verb. Every call takes a context and a request struct; you get back a *Response with the status, body, and headers.
client := http.NewClient(http.Config{
BaseURL: "https://api.example.com",
UserAgent: "demo/1.0.0",
Platform: "cli",
})
resp, err := client.Get(ctx, http.GetRequest{
Request: http.Request{Path: "/users/1"},
})
if err != nil {
return err
}
fmt.Println(resp.StatusCode, string(resp.Body))Request carries the per-request knobs - Path, Query (url.Values), Headers, plus ID and SessionID which are emitted as X-Request-ID / X-Session-ID. GetRequest and the body-carrying PostRequest / PutRequest / PatchRequest embed it:
body, _ := http.JSON(map[string]string{"name": "ada"})
resp, err := client.Post(ctx, http.PostRequest{
Request: http.Request{Path: "/users", ID: "req-123"},
Body: []byte(body),
})
user, err := http.FromJSON[User](resp.Body) // typed decode helperGetStream / PostStream open an SSE connection and decode the wire format into typed StreamResponse values on a channel you own. The call returns once the reader goroutine is running; the channel is closed on EOF.
stream := make(chan http.StreamResponse)
if err := client.GetStream(ctx, stream, http.Request{Path: "/events"}); err != nil {
return err
}
for ev := range stream {
switch ev.Type {
case http.StreamResponseTypeData:
fmt.Println("data:", string(ev.Body))
case http.StreamResponseTypeEvent, http.StreamResponseTypeID, http.StreamResponseTypeRetry:
// event:/id:/retry: lines, value in ev.Body
case http.StreamResponseTypeEOF:
if ev.Error != nil {
return ev.Error
}
}
}Config seeds the client-wide headers used for tracing and client identification, each mapped to a documented header constant (User-Agent, X-Client-Platform, X-Client-Version, X-Client-ID, X-Service-Name). Anything in Config.Headers is sent on every request; per-request Headers override them. The UserAgent(app, version, os, osVersion, arch) helper formats a conventional UA string.
Construction options stay out of your way by default - http.DefaultClient and a silent logger:
client := http.NewClient(cfg,
http.WithHTTPClient(&http.Client{Timeout: 5 * time.Second}), // custom timeout/transport, or a test stub
http.WithLogger(logger), // any leveled logger
)Logger is a minimal Trace/Debug/Info/Warn/Error interface, satisfied structurally by github.com/toaweme/log with no adapter.
The server module wraps net/http.Server behind a chi-backed Router and a {Name, Start, Stop} lifecycle, and keeps chi out of your handlers.
import (
"net/http"
"github.com/toaweme/http/server"
)
r := server.NewRouter()
r.Use(server.SlogMiddleware(server.SlogConfig{}, logger)) // request logging
r.Get("/health", func(w http.ResponseWriter, _ *http.Request) {
server.WriteJSON(w, http.StatusOK, map[string]string{"status": "ok"})
})
r.Get("/items/{id}", func(w http.ResponseWriter, req *http.Request) {
server.WriteJSON(w, http.StatusOK, map[string]string{"id": server.Param(req, "id")})
})
srv := server.NewServer(server.Config{Host: "127.0.0.1", Port: 8080}, r, logger)
if err := srv.Start(); err != nil { // blocks; Stop(ctx) for graceful shutdown
log.Fatal(err)
}Tune the underlying server with options (WithReadHeaderTimeout, WithReadTimeout, WithWriteTimeout, WithIdleTimeout) or reach the raw *http.Server via srv.HTTP() for anything they do not cover (TLS, connection hooks). Bearer-token auth is one middleware away:
r.Use(server.AuthMiddleware(extractClaims, logger))
// inside a handler:
org, _ := server.OrgIDFromContext(req.Context())Broadcast SSE with the hub:
hub := sse.NewHub()
r.Get("/stream", func(w http.ResponseWriter, req *http.Request) {
_ = sse.ServeStream(w, req, hub, "updates")
})
// elsewhere:
hub.Publish("updates", sse.Event{Type: "tick", Data: "hello"})Client (github.com/toaweme/http)
- Zero dependencies - pure stdlib
net/http, nothing transitive. - Struct requests, one method per verb -
Get,Post,Put,Patch,Deletereturning*Response(status, body, headers). - SSE streaming -
GetStream/PostStreamdecodedata:/event:/id:/retry:/comment lines into typedStreamResponsevalues, with explicit EOF and errors. - Config-driven identity - base URL, user-agent, platform, app version, client/service IDs, and custom headers, each behind a documented header constant.
- Per-request overrides - path, query, headers, request ID, session ID.
- Swappable transport -
WithHTTPClientfor custom timeouts/transports or a stub in tests;http.DefaultClientby default. - Injectable logger - leveled
Loggerinterface, silent by default, satisfied structurally bygithub.com/toaweme/log. - JSON helpers -
JSON(v)and genericFromJSON[T](body).
Server (github.com/toaweme/http/server)
- chi-backed router - method helpers, groups, scoped/inline middleware, and route logging, without exposing chi to handlers.
- Server lifecycle -
Name/Start/Stopovernet/http.Serverwith graceful shutdown and functional options (timeouts) plus aHTTP()escape hatch. - Param access -
Param,Wildcard,RoutePattern. - Auth middleware - Bearer-token extraction into request context (org/user/scopes) via a pluggable
ClaimsExtractor. - Request logging - structured method/url/duration/status, with optional headers and size-capped bodies.
- JSON helpers -
WriteJSON,WriteError,WriteBadRequest,ReadJSON,ReadRawJSON. - Local logger interface - defined in the module so the server never depends on the client.
SSE hub (github.com/toaweme/http/server/sse)
- Writer - emits well-formed SSE events (id/event/multi-line data) and flushes.
- Hub - topic fan-out to subscribers; slow subscribers are dropped rather than blocking producers;
ServeStreamties it to a handler with heartbeats.
The test files double as usage references: client_test.go for the client, and the server/ package tests (router_test.go, server_test.go, sse/sse_test.go) for routing, lifecycle, and streaming.
go test ./...