Skip to content

Latest commit

 

History

41 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Sim

Latest Release Tests Lint Go Reference Go 1.26+ License: MIT

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.

Features

Core

  • Zero dependencies — only the Go standard library
  • Method-based routing: Get, Post, Put, Delete, Patch, Options, Head, Connect, Trace, and Any
  • Routing follows the net/http.ServeMux patterns
  • Route groups under a common prefix
  • Standard net/http handlers work everywhere — no framework-specific context type to learn
  • Wrapper composition with Chain and ChainFunc
  • Conditional wrapper application with Selector
  • Request binding: BindJSON, BindXML, BindQuery, BindForm, BindPath and BindHeader
  • Response helpers: JSON, XML, Text, Bytes, Stream and Attachment
  • Graceful shutdown with Run

Built-in wrappers

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.

Request binding

  • 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 via Decoder
  • Read the request body more than once with BufferBody

See the package documentation for the full struct-tag rules.

Response helpers

  • Write JSON, XML, text, byte, streaming, and attachment responses
  • JSON options: EscapeForHTML for safe HTML embedding, Indented for readable output
  • Stream for large or in-progress bodies without loading them into memory, Attachment for file downloads

Installation

Requires Go 1.26+.

go get github.com/qm012/sim

Quick start

package 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")
}

Example

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.go

Open 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}

Contributing

See CONTRIBUTING.md for how to report bugs, suggest features, improve docs, write tests, and submit changes.

Acknowledgements

Sim's design was inspired by:

License

MIT, see LICENSE.

About

A minimal Go web framework that enhances net/http instead of replacing it — middleware & utilities, stdlib handlers intact, native performance. Simple, not simplistic.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

2 stars

Watchers

1 watching

Forks

Releases

Used by

Contributors

Languages