Xun
Xun is a web framework built on Go's built-in html/template and net/http package’s router (1.22).
Install / Use
/learn @yaitoo/XunREADME
Xun AI Agent Specification
Status: AUTHORITATIVE
Before writing any Xun code, read this entire document. All guidance must be derived from this file — do not rely on prior knowledge of gin/echo/chi.
Section 0 — Critical Rules (Read Before Writing Any Code)
These rules are numbered. All other sections reference them by number.
Rule 0.1 — NEVER call WithHandlerViewers() with no arguments
// WRONG — compiles but sets app.handlerViewers = nil
xun.New(xun.WithHandlerViewers())
// CORRECT — pass at least one Viewer
xun.New(xun.WithHandlerViewers(&xun.JsonViewer{}))
Consequence: app.handlerViewers == nil → all handler routes get r.Viewers == nil → c.View(data) returns ErrViewNotFound → HTTP 404.
Rule 0.2 — NEVER write response body directly
// WRONG — bypasses compression and BufPool
c.Response.Write([]byte("hello"))
return nil
// CORRECT — always use c.View()
return c.View("hello") // with StringViewer registered
Rule 0.3 — NEVER return an error from middleware when refusing
// WRONG — returns 500 + X-Log-Id
func refuseMiddleware(next xun.HandleFunc) xun.HandleFunc {
return func(c *xun.Context) error {
if !allowed {
return errors.New("forbidden")
}
return next(c)
}
}
// CORRECT — set status and return ErrCancelled
func refuseMiddleware(next xun.HandleFunc) xun.HandleFunc {
return func(c *xun.Context) error {
if !allowed {
c.WriteStatus(http.StatusForbidden)
return xun.ErrCancelled
}
return next(c)
}
}
Rule 0.4 — app.Start() does NOT start the server
// WRONG — gin habit
app.Run(":8080")
// CORRECT
app := xun.New(opts...)
app.Start() // only prints route logs
defer app.Close()
http.ListenAndServe(":80", mux) // server startup is caller's responsibility
Rule 0.5 — Named viewer MUST match Accept header or silently falls back
// Route: r.Viewers = [JsonViewer]
// Request: Accept: application/json
return c.View(user, "views/user/profile") // views/user/profile is HtmlViewer
// HtmlViewer (text/html) does NOT match Accept (application/json)
// → falls back to JsonViewer (r.Viewers[0]), NOT the named viewer
Rule 0.6 — pages/* auto-registers GET only
// File: pages/admin/dashboard.html
// Route: GET /admin/dashboard ← GET only, no POST/PUT/DELETE auto-registered
// To handle POST, register explicitly:
app.Post("/admin/dashboard", handler)
Rule 0.7 — {$} means trailing slash required
app.Get("/posts/{$}") // matches GET /posts/ ONLY
app.Get("/posts/") // matches GET /posts/abc, GET /posts/123
app.Get("/posts") // matches GET /posts ONLY (no slash)
Section 1 — Types
HandleFunc = func(c *Context) error
Middleware = func(next HandleFunc) HandleFunc
Option = func(*App)
RoutingOption = func(*RoutingOptions)
chain = interface{ Next(hf HandleFunc) HandleFunc }
HandleFunc returns error, not nil. See Section 10 for error meanings.
Section 2 — App
2.1 Creation
app := xun.New(opts ...Option) *App
2.2 Fields
| Field | Type | Default | Overridden-By | Nil-Result |
|-------|------|---------|---------------|------------|
| app.mux | *http.ServeMux | http.DefaultServeMux | WithMux(mux) | — |
| app.handlerViewers | []Viewer | []Viewer{&JsonViewer{}} | WithHandlerViewers(v...) | All handler routes return 404 (Rule 0.1) |
| app.fsys | fs.FS | nil | WithFsys(fsys) | Page routing disabled |
| app.watch | bool | false | WithWatch() | Hot reload disabled |
| app.interceptor | Interceptor | nil | WithInterceptor(i) | Redirect/RequestReferer use defaults |
| app.compressors | []Compressor | nil | WithCompressor(c...) | No compression |
| app.viewers | map[string]Viewer | empty map | HtmlViewEngine.Load() registers views/* | Named viewers unavailable |
| app.funcMap | template.FuncMap | xun.builtins | WithTemplateFunc, WithTemplateFuncMap | Builtin asset func unavailable |
| app.routes | map[string]*Routing | empty map | app.Get/Post/etc, app.HandlePage | — |
2.3 App.Start()
app.Start()
Writes info-level logs for each registered route (pattern + viewer MIME types). Does NOT start the HTTP server. Server startup is the caller's responsibility.
2.4 App.Close()
Currently a no-op. Reserved for future use.
2.5 Option Functions
WithMux(mux *http.ServeMux) Option
WithFsys(fsys fs.FS) Option
WithWatch() Option // dev only — not thread-safe
WithHandlerViewers(v ...Viewer) Option
WithViewEngines(ve ...ViewEngine) Option
WithInterceptor(i Interceptor) Option
WithCompressor(c ...Compressor) Option
WithTemplateFunc(name string, fn any) Option
WithTemplateFuncMap(fm template.FuncMap) Option
WithBuildAssetURL(match func(string) bool) Option
WithLogger(logger *slog.Logger) Option
2.6 Route Registration
app.Get(pattern string, hf HandleFunc, opts ...RoutingOption)
app.Post(pattern string, hf HandleFunc, opts ...RoutingOption)
app.Put(pattern string, hf HandleFunc, opts ...RoutingOption)
app.Delete(pattern string, hf HandleFunc, opts ...RoutingOption)
app.Group(prefix string) *group
Pattern format: "METHOD pattern" (e.g., "GET /users/{id}"). Go 1.22 ServeMux syntax.
Section 3 — Group
group implements chain.
func (g *group) Use(middleware ...Middleware)
func (g *group) Get(pattern string, hf HandleFunc, opts ...RoutingOption)
func (g *group) HandleFunc(pattern string, hf HandleFunc, opts ...RoutingOption)
func (g *group) Next(hf HandleFunc) HandleFunc
Middleware chain construction (inside-out):
// given [A, B, C] and handler H:
// build: C(B(A(H)))
next := H
for i := len(g.middlewares); i > 0; i-- {
next = g.middlewares[i-1](next)
}
Section 4 — Middleware
Middleware signature: func(next HandleFunc) HandleFunc
func AuthMiddleware(next xun.HandleFunc) xun.HandleFunc {
return func(c *xun.Context) error {
// pre logic
token := c.Request.Header.Get("X-Token")
if token == "" {
c.WriteStatus(http.StatusUnauthorized)
return xun.ErrCancelled
}
err := next(c)
// post logic (runs after handler)
return err
}
}
Pre-logic: runs before next(c). Post-logic: runs after next(c) returns.
On refusal: ALWAYS set status + return xun.ErrCancelled (Rule 0.3).
Section 5 — Context
Context wraps *http.Request, ResponseWriter, and application state.
5.1 Fields
c.Request *http.Request // standard library
c.Response ResponseWriter // xun interface (extends http.ResponseWriter)
c.Routing Routing // route metadata
c.App *App // application instance
c.TempData TempData // map[string]any, request-scoped storage
5.2 Standard Library Equivalents
Use standard library directly for these:
c.Request.PathValue("id") // path parameter (Go 1.22+)
c.Request.URL.Query().Get("name") // query string
c.Request.Header.Get("X-Token") // headers
c.Request.Cookie("session_id") // read cookie
c.Request.Body // request body
c.Request.ParseMultipartForm() // multipart form
c.Request.Context() // context.Context
c.Response.Header().Set(k, v) // response headers
http.SetCookie(c.Response, &cookie) // write cookie
c.Request.RemoteAddr // client address (no proxy support; use ext/proxyproto)
5.3 xun-Specific Methods
c.View(data any, options ...string) error
c.Redirect(url string, statusCode ...int)
c.AcceptLanguage() []string
c.Accept() []MimeType
c.RequestReferer() string
c.WriteStatus(code int)
c.WriteHeader(key string, value string)
c.Get(key string) any
c.Set(key string, value any)
5.4 c.View(data any, options ...string) Behavior
IF options[0] is provided (named viewer name):
→ getViewer(name) checks: named viewer.MimeType() matches any Accept header
→ IF match: use named viewer
→ IF no match: proceed to step 2
ELSE skip to step 2.
STEP 2: Iterate Accept headers, match against r.Viewers:
→ First matching viewer is used
STEP 3: No match found:
→ Use r.Viewers[0] as fallback
STEP 4: r.Viewers is empty at this point:
→ Return ErrViewNotFound → HTTP 404
c.View() sets status 200 automatically. Call c.WriteStatus() before c.View() to override.
5.5 c.Redirect(url string, statusCode ...int)
Sets Location header. Default status: http.StatusFound (302). Interceptor can override if configured.
Section 6 — Routing
6.1 Routing Fields
type Routing struct {
Pattern string
Handle HandleFunc
chain chain // *App or *group
Options *RoutingOptions
Viewers []Viewer // viewers for this route
}
6.2 Routing.Next(ctx)
func (r *Routing) Next(ctx *Context) error {
return r.chain.Next(r.Handle)(ctx)
}
6.3 RoutingOptions Fields
type RoutingOptions struct {
metadata map[string]any
viewers []Viewer
}
6.4 RoutingOption Functions
WithViewer(v ...Viewer) RoutingOption
WithMetadata(key string, value any) RoutingOption
WithNavigation(name, icon, access string) RoutingOption
Section 7 — Viewer
7.1 Interface
type Viewer interface {
MimeType() *MimeType
Render(ctx *Context, data any) error
}
7.2 Built-in Viewers
| Viewer | MimeType | Default For |
|--------|----------|------------|
| HtmlViewer | text/html | Page routes |
| JsonViewer | application/json | Handler routes (only if app.handlerViewers not overridden) |
| TextViewer | text/* (from filename) | Text templates |
| XmlViewer | text/xml | — |
| StringViewer | text/plain | — |
| `FileV
