restruct is a high-performance, service-oriented REST framework for Go. It allows you to build APIs by simply defining structs and methods, automating routing, parameter binding, validation, and view rendering.
- Key Features
- Install
- Quick Start
- Core Concepts
- Request & Response
- Views & Template Rendering
- Middleware
- Context Values
- Benchmarks
- License
- Struct-Based Routing: Exported methods are automatically mapped to routes via a Trie-based router.
- Hierarchical Services: Nest structs to create path hierarchies (e.g.,
/api/v1/...). - Smart Binding: Auto-bind JSON, Form, Query, and Multipart parameters to struct arguments.
- Interface Driven: Customize behavior via
Router,Writer,Init, andMiddlewaresinterfaces. - View Engine: Integrated template engine with
fs.FSsupport, layout templates, and error page fallbacks. - Zero-Boilerplate: Focus on business logic, let the framework handle the plumbing.
go get github.com/altlimit/restructpackage main
import (
"net/http"
"github.com/altlimit/restruct"
)
type Calculator struct{}
// POST /add
// Payload: {"a": 10, "b": 20}
func (c *Calculator) Add(req struct {
A int `json:"a"`
B int `json:"b"`
}) int {
return req.A + req.B
}
func main() {
restruct.Handle("/", &Calculator{})
http.ListenAndServe(":8080", nil)
}Any exported struct can be a service. Public methods become endpoints.
func (s *Svc) Index()->/func (s *Svc) Users()->/usersfunc (s *Svc) Users_0()->/users/{0}func (s *Svc) UsersExport()->/users-exportfunc (s *Svc) Any()->/{any*}(Wildcard catch-all)func (s *Svc) Files_Any()->/files/{any*}(Scoped wildcard)
You can define path parameters and wildcards using special method naming conventions or the Router interface.
Method Naming:
Find_0_User->/find/{0}/user(path parameter)Files_Any->/files/{any*}(Wildcard catch-all)
Explicit Routing (Router Interface):
Recommended for cleaner method names and explicit HTTP method restrictions.
Handler accepts a string (method name) or a func (used directly):
func (u *User) Routes() []restruct.Route {
return []restruct.Route{
{Handler: u.CreateUser, Path: ".", Methods: []string{"POST"}},
{Handler: "ReadUser", Path: "{id}", Methods: []string{"GET"}},
{Handler: "UpdateUser", Path: "{id}", Methods: []string{"PUT"}},
{Handler: "DeleteUser", Path: "{id}", Methods: []string{"DELETE"}},
}
}Handler: "MethodName"— Resolves to the struct method by name.Handler: u.MethodNameorHandler: myFunc— Uses the func directly.Path: "."maps to the service root path (useful for CRUD on collection endpoints).- Omitting
Pathuses the default naming convention. - Omitting
Methodsallows all HTTP methods. Middlewareson a Route applies per-route middleware.
Path parameters can be accessed via restruct.Params(r)["id"] or restruct.Vars(ctx)["id"].
Services can be nested to create API versions or groups.
type Server struct {
ApiV1 V1 `route:"api/v1"` // Mounts V1 at /api/v1
Admin AdminService `route:"admin"`
DB struct{} `route:"-"` // Ignored
}restruct binds request data (JSON body, Form data, Query params) to method arguments automatically via the RequestReader interface. The DefaultReader dispatches to the Bind function which supports:
application/json->BindJson(usesjsonstruct tag)application/x-www-form-urlencoded/multipart/form-data->BindForm(usesformstruct tag)- Query parameters ->
BindQuery(usesquerystruct tag)
You can extend the DefaultReader with a custom Bind function to add validation (e.g., using go-playground/validator):
func (s *Server) Init(h *restruct.Handler) {
h.Reader = &restruct.DefaultReader{
Bind: func(r *http.Request, out interface{}, methods ...string) error {
if err := restruct.Bind(r, out, methods...); err != nil {
return err
}
return validate.Struct(out)
},
}
}The ResponseWriter interface controls how handler return values are written to the response.
Handlers can return:
struct/map/slice: Encoded as JSON byDefaultWriter.string/[]byte: Sent as raw response.error: Converted to appropriate HTTP error status.*restruct.Response: Complete control over Status, Headers, ContentType, and Content bytes.*restruct.Json: JSON response with a custom status code (e.g.,restruct.Json{Status: 201, Content: obj}).*restruct.Render: Force rendering a specific template path (see Explicit Template Rendering).(int, any, error): Status code, response body, and error.(any, error): Response body with error handling.
The DefaultWriter supports:
ErrorHandler func(error) any— Custom error formatting. Return*restruct.Responsefor full control.Errors map[error]Error— Map known errors to custom HTTP statuses/messages.EscapeJsonHtml bool— Control HTML escaping in JSON output.
Implement the Writer interface to enable HTML rendering and asset serving for your service. This associates a ResponseWriter (typically a *View) with the service.
func (s *Server) Writer() restruct.ResponseWriter {
return &restruct.View{
FS: publicFS, // fs.FS interface
Funcs: template.FuncMap{"upper": strings.ToUpper},
Skips: regexp.MustCompile("^layout"), // Skip layout files from routing
Layouts: []string{"layout/*.html"}, // Layout template patterns
Error: "error.html", // Error page template
Data: func(r *http.Request) map[string]any {
return map[string]any{"SiteName": "My Site"}
},
}
}- If a method returns a struct/map,
restructfirst checks for a matching template (e.g.,index.htmlforIndexmethod). - If no template is found, it delegates to the fallback
Writeror falls back to JSON. - Templates receive
{{.Request}}, path parameters, handler return data, and data from theDatacallback.
You can easily serve embedded assets.
//go:embed public
var publicFS embed.FS
func (s *Server) Writer() restruct.ResponseWriter {
sub, _ := fs.Sub(publicFS, "public")
return &restruct.View{
FS: sub,
// ...
}
}Use *restruct.Render to force rendering a specific template, regardless of the request URL:
func (s *Service) Dashboard(ctx context.Context) (*restruct.Render, error) {
userID := restruct.Vars(ctx)["id"]
if userID == "" {
return nil, errors.New("user not found")
}
return &restruct.Render{
Path: "dashboard/main.html",
Data: map[string]interface{}{
"UserID": userID,
"Title": "Dashboard",
},
}, nil
}Path: Template path relative to the View's FS root.Data: Data passed to the template (merged ifmap[string]any, otherwise accessible as{{.Data}}).
Middleware can be applied globally, per-service, or per-route using the standard func(http.Handler) http.Handler signature.
func (s *Server) Init(h *restruct.Handler) {
h.Use(restruct.Recovery) // Built-in panic recovery middleware
h.Use(loggingMiddleware)
}Per-service middleware via the Middlewares interface:
func (b *Blob) Middlewares() []restruct.Middleware {
return []restruct.Middleware{loggerMiddleware}
}Per-route middleware via the Route struct:
{Handler: "Upload", Middlewares: []restruct.Middleware{authMiddleware}}Built-in: restruct.Recovery — Panic recovery middleware that logs the stack trace and returns a 500 error.
Store and retrieve values in the request context (useful for passing data from middleware to handlers):
// Using *http.Request
r = restruct.SetValue(r, "userID", int64(1))
userID := restruct.GetValue(r, "userID").(int64)
allVals := restruct.GetValues(r) // map[string]interface{}
// Using context.Context
ctx = restruct.SetVal(ctx, "key", value)
val := restruct.GetVal(ctx, "key")
allVals := restruct.GetVals(ctx) // map[string]interface{}// Register on http.DefaultServeMux
restruct.Handle("/api/", &MyService{})
// Or create a Handler for use with custom routers
h := restruct.NewHandler(&MyService{})
h.WithPrefix("/api/")
h.AddService("extra/", &ExtraService{})
// List all registered routes (useful for debugging)
for _, route := range h.Routes() {
fmt.Println(route)
}High performance with minimal overhead. (See bench_test.go for latest results).
MIT