Not another web framework — the missing layer on top of net/http.
Sim (short for simple) is a minimal HTTP web framework for Go, built on
top of net/http and http.ServeMux — no third-party dependencies. It
adds wrappers and utilities while keeping stdlib handlers intact and
native performance untouched. Simple, not simplistic.
- Zero dependencies — only the Go standard library
- Method-based routing:
Get,Post,Put,Delete,Patch,Options,Head,Connect,Trace, andAny - Routing follows the net/http.ServeMux patterns
- Route groups under a common prefix
- Standard
net/httphandlers work everywhere — no framework-specific context type to learn - Wrapper composition with
ChainandChainFunc - Conditional wrapper application with
Selector - Request binding:
BindJSON,BindXML,BindQuery,BindForm,BindPathandBindHeader - Response helpers:
JSON,XML,Text,Bytes,StreamandAttachment - Graceful shutdown with
Run
| Wrapper | What it does |
|---|---|
ClientIPResolution |
Resolves the real client IP behind trusted proxies and adds client_ip to request logs |
RequestLogging |
Writes structured slog records per request |
Recovery |
Turns panics into a logged stack trace and HTTP 500 instead of a crash |
Default bundles all three wrappers, ready to use with no configuration.
- Bind incoming request data into your own structs from JSON, XML, query, form, path, and header values.
- Struct tags with
default=values, embedded structs, multipart file uploads, and map targets - Validation via
Validator, custom formats viaDecoder - Read the request body more than once with
BufferBody
See the package documentation for the full struct-tag rules.
- Write JSON, XML, text, byte, streaming, and attachment responses
- JSON options:
EscapeForHTMLfor safe HTML embedding,Indentedfor readable output Streamfor large or in-progress bodies without loading them into memory,Attachmentfor file downloads
Requires Go 1.26+.
go get github.com/qm012/simpackage main
import (
"context"
"net/http"
"github.com/qm012/sim"
)
func main() {
app := sim.Default()
app.Get("/", func(w http.ResponseWriter, _ *http.Request) {
_ = sim.Text(w, http.StatusOK, "hello, sim")
})
_ = app.Run(context.Background(), ":8080")
}A complete runnable REST API:
package main
import (
"context"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"github.com/qm012/sim"
)
func main() {
// NewApp starts with no wrappers; register them explicitly,
// outermost first.
app := sim.NewApp()
logging := new(sim.RequestLogging)
app.Use(
new(sim.ClientIPResolution).Handler,
// Log every request except the ping endpoint.
sim.Selector(logging.Handler, func(r *http.Request) bool {
return r.URL.Path != "/ping"
}),
new(sim.Recovery).Handler,
)
app.Get("/", func(w http.ResponseWriter, _ *http.Request) {
_ = sim.Text(w, http.StatusOK, "welcome")
})
app.Any("/ping", func(w http.ResponseWriter, _ *http.Request) {
_ = sim.Text(w, http.StatusOK, "pong")
})
// Group routes under a common prefix.
app.Group("/api", func(r sim.Router) {
r.Get("/users", listUsers)
r.Get("/users/{id}", getUser)
r.Post("/users", createUser)
r.Put("/users/{id}", updateUser)
r.Delete("/users/{id}", deleteUser)
})
// Compose wrappers with Chain / ChainFunc.
app.Get("/admin", sim.ChainFunc(auth)(adminPanel))
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer cancel()
if err := app.Run(ctx, ":8080"); err != nil {
log.Fatal(err)
}
}
func auth(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("Authorization") == "" {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
next.ServeHTTP(w, r)
})
}
func listUsers(w http.ResponseWriter, r *http.Request) {
// BindQuery fills a struct from the URL query; page defaults to 1.
q, err := sim.BindQuery[struct {
Page int `query:"page,default=1"`
}](r)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
_ = sim.JSON(w, http.StatusOK, struct {
Page int `json:"page"`
Users []user `json:"users"`
}{q.Page, []user{
{Name: "alice", Age: 30},
{Name: "bob", Age: 25},
}})
}
func getUser(w http.ResponseWriter, r *http.Request) {
// BindPath fills a struct from path values.
p, err := sim.BindPath[struct {
ID string `path:"id"`
}](r)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
_ = sim.JSON(w, http.StatusOK, user{ID: p.ID, Name: "alice", Age: 30})
}
// user is the payload the API exchanges with its clients.
type user struct {
ID string `json:"id,omitempty"`
Name string `json:"name"`
Age int `json:"age"`
}
func createUser(w http.ResponseWriter, r *http.Request) {
u, err := sim.BindJSON[user](r)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
_ = sim.JSON(w, http.StatusCreated, u)
}
func updateUser(w http.ResponseWriter, r *http.Request) {
p, err := sim.BindPath[struct {
ID string `path:"id"`
}](r)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
u, err := sim.BindJSON[user](r)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
u.ID = p.ID
_ = sim.JSON(w, http.StatusOK, u)
}
func deleteUser(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusNoContent)
}
func adminPanel(w http.ResponseWriter, _ *http.Request) {
_ = sim.Text(w, http.StatusOK, "admin")
}Save it as main.go and run it:
go run main.goOpen http://localhost:8080/ to see "welcome", and http://localhost:8080/api/users for the user list. The endpoints return JSON. Try them:
curl 'localhost:8080/api/users?page=2'
# {"page":2,"users":[{"name":"alice","age":30},{"name":"bob","age":25}]}
curl localhost:8080/api/users/1
# {"id":"1","name":"alice","age":30}
curl -X POST localhost:8080/api/users \
-H 'Content-Type: application/json' \
-d '{"name":"alice","age":30}'
# {"name":"alice","age":30}
curl -X PUT localhost:8080/api/users/1 \
-H 'Content-Type: application/json' \
-d '{"name":"alice","age":31}'
# {"id":"1","name":"alice","age":31}See CONTRIBUTING.md for how to report bugs, suggest features, improve docs, write tests, and submit changes.
Sim's design was inspired by:
MIT, see LICENSE.