egor
Package egor(enhanced go router) implements a minimalistic but robust http router based on the standard go 1.22 enhanced routing capabilities in the http.ServeMux
. It adds features like middleware support, helper methods for defining routes, template rendering with automatic template inheritance (of a base template).
It also has a BodyParser that decodes json, xml, url-encoded and multipart forms
based on content type. Form parsing supports all standard go types(and their pointers)
and slices of standard types. It also supports custom types that implement the egor.FormScanner
interface.
Installation
go get -u github.com/abiiranathan/egor
Example of a custom type that implements the FormScanner interface
type FormScanner interface {
FormScan(value interface{}) error
}
type Date time.Time // Date in format YYYY-MM-DD
// FormScan implements the FormScanner interface
func (d *Date) FormScan(value interface{}) error {
v, ok := value.(string)
if !ok {
return fmt.Errorf("value is not a string")
}
t, err := time.Parse("2006-01-02", v)
if err != nil {
return fmt.Errorf("invalid date format")
}
*d = Date(t)
return nil
}
egor supports single page application routing with a dedicated method r.SPAHandler
that serves the index.html file for all routes that do not match a file or directory in the root directory of the SPA.
The router also supports route groups and subgroups with middleware that can be applied to the entire group or individual routes.
It has customizable built-in middleware for logging using the slog package, panic recovery, etag, cors, basic auth and jwt middlewares.
More middlewares can be added by implementing the Middleware type, a standard function that wraps an http.Handler.
See the middleware package for examples.
package main
import (
"fmt"
"net/http"
"os"
"time"
"github.com/abiiranathan/egor/egor"
"github.com/abiiranathan/egor/egor/middleware"
)
func main() {
r := egor.NewRouter()
r.Use(egor.Logger(os.Stdout))
r.Use(egor.Recovery(true))
r.Use(egor.Cors())
r.Use(egor.ETag())
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "Hello, World!")
})
r.Get("/hello/:name", func(w http.ResponseWriter, r *http.Request) {
name := r.PathValue("name")
fmt.Fprintf(w, "Hello, %s!", name)
// r.SendString(fmt.Sprintf("Hello, %s!", name))
})
admin := r.Group("/admin", AdminRequired)
admin.Get("/dashboard", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "Admin Dashboard")
})
api := r.Group("/api")
api.Get("/users", func(w http.ResponseWriter, r *http.Request) {
users := []string{"John", "Doe", "Jane"}
r.SendJSON(w, users)
})
}
Rendering Templates
See the example for a complete example of how to use the egor package.
package main
import (
"github.com/abiiranathan/egor/egor"
"embed"
"log"
"net/http"
"text/template"
)
//go:embed templates
var viewsFS embed.FS
// base.html is automatically added to every template.
// {{ .Content }} is replaced with page contents.
// No need for {{ template "base.html" . }} in every page.
func HomeHandler(w http.ResponseWriter, req *http.Request) {
data := map[string]any{
"Title": "Home Page",
"Body": "Welcome to the home page",
}
// Router is accessed in context and used for rending. Same as r.Render()
// but this way you don't need r in scope.
egor.Render(w, req, "home.html", data)
}
func AboutHandler(w http.ResponseWriter, req *http.Request) {
data := map[string]any{
"Title": "About Page",
"Body": "Welcome to the about page",
}
egor.Render(w, req, "about.html", data)
}
func NestedTemplate(w http.ResponseWriter, req *http.Request) {
egor.Render(w, req, "doctor/doctor.html", map[string]any{})
}
func main() {
templ, err := egor.ParseTemplatesRecursiveFS(viewsFS, "templates", template.FuncMap{}, ".html")
if err != nil {
panic(err)
}
/*
OR
templ, err := egor.ParseTemplatesRecursive(viewsDirname, template.FuncMap{}, ".html")
if err != nil {
panic(err)
}
*/
r := egor.NewRouter(
egor.WithTemplates(templ),
egor.PassContextToViews(true),
egor.BaseLayout("base.html"),
egor.ContentBlock("Content"),
)
r.Get("/", HomeHandler)
r.Get("/about", AboutHandler)
r.Get("/doctor", NestedTemplate)
srv := egor.NewServer(":8080", r)
log.Fatalln(srv.ListenAndServe())
}
No external libraries are included in the main package. Only a few external libraries are used in the middleware package.
Helper functions at package level
-
func SetContextValue(req *http.Request, key any, value interface{})
-
func GetContextValue(req *http.Request, key any) interface{}
-
func SendJSON(w http.ResponseWriter, data interface{}) error
-
func SendString(w http.ResponseWriter, data string) error
-
func SendHTML(w http.ResponseWriter, html string) error
-
func SendFile(w http.ResponseWriter, req *http.Request, file string)
-
func SendError(w http.ResponseWriter, err error, status int)
-
func SendJSONError(w http.ResponseWriter, key, s string, status int)
-
func GetContentType(req *http.Request) string
-
func Redirect(req *http.Request, w http.ResponseWriter, url string, status ...int)
-
func Query(req *http.Request, key string, defaults ...string) string
-
func QueryInt(req *http.Request, key string, defaults ...int) int
-
func ParamInt(req *http.Request, key string, defaults ...int) int
-
func SaveFile(fh *multipart.FileHeader, dst string) error
-
func FormValue(req *http.Request, key string) string
-
func FormData(req *http.Request) url.Values
-
func FormFile(req *http.Request, key string) (*multipart.FileHeader, error)
-
func FormFiles(req *http.Request, key string) ([]*multipart.FileHeader, error)
-
func ParseMultipartForm(req *http.Request, maxMemory ...int64) (*multipart.Form, error)
-
func BodyParser(req *http.Request, v interface{}) error
Tests
go test -v ./...
Benchmarks
go test -bench=. ./... -benchmem
Contributing
Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change.
Please make sure to update tests as appropriate.
License
MIT