net/http middleware with a type-driven Handler interface, handling content negotiation automatically.
The default net/http handler interface:
ServeHTTP(http.ResponseWriter, *http.Request)rsvp's handler interface looks like this:
type Body struct {
Data any,
}
ServeHTTP(ResponseWriter, *http.Request) Body- Content Negotiation. rsvp will attempt to provide Data in a supported media type that is requested via the Accept header; or even the URL's file extension in the case of GET requests:
-
application/json -
text/html -
text/plain -
text/csv(by implementing the rsvp.Csv interface) -
application/octet-stream -
application/xml -
application/vnd.golang.gob(Golang's encoding/gob) -
application/vnd.msgpack(optional extension behind -tags=rsvp_msgpack) - Others to be implemented?
-
- Extension matching on GET requests:
/users/123→ Returns default media type (determined by the value of Body)/users/123.json→ Forcesapplication/json/users/123.xml→ Forcesapplication/xml/users/123.csv→ Forcestext/csv- NOTE: Currently, this behaviour is hidden behind net/http's strict path matching. The above examples would require something like
mux.Handle("/users/{filename}")with middleware that strips out the file extension, and matches the remaining file stem with its respective handler. rsvp does not currently provide a utility for this.
It's easy for me to lose track of what I've written to http.ResponseWriter. Occasionally receiving the old http: multiple response.WriteHeader calls
With this library I just return a value, which I can only ever do once, to execute an HTTP response write. Why write responses with a weird mutable reference from goodness knows where? YEUCH!
Having to remember to return separately from resolving the response? *wretch*
if r.Method != http.MethodPut {
http.Error(w, "Use PUT", http.StatusMethodNotAllowed)
return
}Not with rsvp 🫠
if r.Method != http.MethodPut {
return rsvp.Data("Use PUT").StatusMethodNotAllowed()
}(Wrapping this with your own convenience method, i.e. func ErrorMethodNotAllowed(message string) rsvp.Body is encouraged. You can decide for yourself how errors are represented)
| Feature | net/http | Gin / Echo / Fiber | rsvp |
|---|---|---|---|
| Response Style | Imperative (w.Write) | Context-based (c.JSON) | Value-Oriented (return) |
| Content Negotiation | Manual | Manual / Middleware | Built-in & Automatic |
| URL Extensions | Manual parsing | Generally unsupported | Native (.json, .csv) |
| Response Handling | Easy to forget return | Side-effect based | Compile-time enforced |
| Size | Standard minimum | Massive abstraction | Lightweight Wrapper |
You value Progressive Enhancement: You want your API to be easily browsable by a human (HTML/XML) but consumable by a script (JSON/CSV) using the same URL.
You hate "Multiple WriteHeader" logs: You want a handler signature that makes it impossible to write a partial or double response.
You want trivially testable handlers: Since handlers return a struct, you can unit test your logic by inspecting the returned rsvp.Response instead of mocking a whole http.ResponseWriter.
High-performance binary streaming: If you are streaming gigabytes of data where every nanosecond of overhead matters, the abstraction of rsvp struct might not be for you.
OpenAPI-first workflows: If your primary goal is generating documentation from code, a schema-heavy framework like Huma might be a better fit. Although you might want to look into creating a proper RESTful self-documenting interface by combining rsvp with hyprctl: https://github.com/Teajey/hyprctl
func main() {
mux := http.NewServeMux()
cfg := rsvp.Config{}
mux.HandleFunc("GET /users/{id}", rsvp.AdaptHandlerFunc(cfg, getUser))
http.ListenAndServe(":8080", mux)
}
func getUser(w rsvp.ResponseWriter, r *http.Request) rsvp.Body {
return rsvp.Data(User{ID: 123}) // In content negotiation this will be offered as, in order; JSON, XML, and encoding/gob.
}Important
nil Data renders as JSON "null\n", not an empty response.
Use rsvp.Data("") for a blank text/plain response body, or rsvp.Blank() for a blank response with no Content-Type.
rsvp.Config{
HtmlTemplate: template.Must(template.ParseGlob("templates/html/*.gotmpl")),
TextTemplate: template.Must(template.ParseGlob("templates/text/*.gotmpl")),
}
func showUser(w rsvp.ResponseWriter, r *http.Request) rsvp.Body {
w.DefaultTemplateName("user.gotmpl") // Must exist in HtmlTemplate and/or TextTemplate for formats to match
return rsvp.Data(User{ID: 123}) // In content negotiation this will be offered as JSON, XML, HTML, plain text, and encoding/gob.
}Build your own!
type APIError struct {
Message string `json:"message"`
Code string `json:"code"`
}
func ErrorNotFound(msg string) rsvp.Body {
return rsvp.Data(APIError{Message: msg, Code: "NOT_FOUND"}).StatusNotFound()
}type UserList []User
var users UserList
func (ul UserList) MarshalCsv(w *csv.Writer) error {
w.Write([]string{"ID", "Name", "Email"})
for _, u := range ul {
w.Write([]string{u.ID, u.Name, u.Email})
}
return nil
}
func userList(w rsvp.ResponseWriter, r *http.Request) rsvp.Body {
return rsvp.Data(users) // In content negotiation this will be offered as JSON, XML, CSV, and encoding/gob.
}See middleware_test.go for an example of how to use this library with standard middleware.
You can see it in action on my stupid little blog site, brightscroll.net. For instance, https://brightscroll.net/posts/2025-06-30.md vs. https://brightscroll.net/posts/2025-06-30.md.txt