Routing & Handlers
irgo uses a chi-based router with a simplified API designed for HTMX applications. Handlers return HTML strings rather than writing to response writers.
Basic Routing
import "github.com/stukennedy/irgo/pkg/router"r := router.New()// Basic routesr.GET("/", HomeHandler)r.POST("/users", CreateUserHandler)r.PUT("/users/{id}", UpdateUserHandler)r.DELETE("/users/{id}", DeleteUserHandler)r.PATCH("/users/{id}", PatchUserHandler)Fragment Handlers
Handlers in irgo return (string, error) instead of writing tohttp.ResponseWriter. This makes them cleaner and easier to test:
func HomeHandler(ctx *router.Context) (string, error) { return renderer.Render(templates.HomePage())}func UserHandler(ctx *router.Context) (string, error) { id := ctx.Param("id") user, err := db.GetUser(id) if err != nil { return "", ctx.NotFound("User not found") } return renderer.Render(templates.UserProfile(user))}URL Parameters
Use curly braces for URL parameters:
// Single parameterr.GET("/users/{id}", func(ctx *router.Context) (string, error) { id := ctx.Param("id") return fmt.Sprintf("User ID: %s", id), nil})// Multiple parametersr.GET("/posts/{postID}/comments/{commentID}", func(ctx *router.Context) (string, error) { postID := ctx.Param("postID") commentID := ctx.Param("commentID") return fmt.Sprintf("Post %s, Comment %s", postID, commentID), nil})Query Parameters
// GET /search?q=hello&limit=10r.GET("/search", func(ctx *router.Context) (string, error) { query := ctx.Query("q") limit := ctx.Query("limit") if query == "" { return "", ctx.BadRequest("Query parameter 'q' is required") } results := search(query, limit) return renderer.Render(templates.SearchResults(results))})Form Data
r.POST("/users", func(ctx *router.Context) (string, error) { name := ctx.FormValue("name") email := ctx.FormValue("email") if name == "" || email == "" { return "", ctx.BadRequest("Name and email are required") } user := db.CreateUser(name, email) return renderer.Render(templates.UserCard(user))})Route Groups
Organize related routes with groups:
r.Route("/api", func(r *router.Router) { // All routes here are prefixed with /api r.GET("/users", ListUsers) r.POST("/users", CreateUser) r.Route("/admin", func(r *router.Router) { // /api/admin/... r.GET("/stats", AdminStats) r.POST("/reset", AdminReset) })})Context API
The router.Context provides methods for handling requests and responses:
Input Methods
func handler(ctx *router.Context) (string, error) { // URL parameters id := ctx.Param("id") // Query string parameters query := ctx.Query("q") // Form values name := ctx.FormValue("name") // Request headers token := ctx.Header("Authorization") // Access underlying request req := ctx.Request return "", nil}HTMX Detection
func handler(ctx *router.Context) (string, error) { // Check if request is from HTMX if ctx.IsHTMX() { // Return partial HTML return renderer.Render(templates.UserCard(user)) } // Return full page return renderer.Render(templates.UserPage(user))}// HTMX request headerstarget := ctx.HXTarget() // HX-Target headertrigger := ctx.HXTrigger() // HX-Trigger headercurrentURL := ctx.HXCurrentURL() // HX-Current-URL headerResponse Methods
// HTML responsesctx.HTML("<div>Hello</div>")ctx.HTMLStatus(201, "<div>Created</div>")// JSON responsesctx.JSON(user)ctx.JSONStatus(201, user)// Redirects (HTMX-aware)ctx.Redirect("/new-location")// No contentctx.NoContent()Error Responses
// Generic errorctx.Error(err)ctx.ErrorStatus(500, "Internal error")// Common HTTP errorsctx.NotFound("Resource not found")ctx.BadRequest("Invalid input")ctx.Unauthorized("Please log in")ctx.Forbidden("Access denied")HTMX Response Headers
Control HTMX behavior with response headers:
func handler(ctx *router.Context) (string, error) { // Update browser URL ctx.PushURL("/users/123") ctx.ReplaceURL("/users/123") // Trigger client-side events ctx.Trigger("userCreated") ctx.TriggerAfterSettle("formReset") ctx.TriggerAfterSwap("showNotification") // Change swap behavior ctx.Retarget("#different-element") ctx.Reswap("innerHTML") // Force full page refresh ctx.Refresh() return renderer.Render(templates.UserCard(user))}Out-of-Band Swaps
Use hx-swap-oob="true" in your templates to update multiple elements with a single response. This is great for updating counters, notifications, or other elements outside the main target.
Static Files
// Serve static files from a directoryr.Static("/static", http.Dir("static"))// Or use http.FileServer directlymux := http.NewServeMux()mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("static"))))mux.Handle("/", r.Handler())Middleware
irgo includes common middleware:
import "github.com/stukennedy/irgo/pkg/router/middleware"r := router.New()// Detect HTMX requests (sets ctx.IsHTMX())r.Use(middleware.HXRequestMiddleware)// Wrap non-HTMX responses in layoutr.Use(middleware.LayoutWrapper(templates.Layout))// Prevent cachingr.Use(middleware.NoCacheMiddleware)// CORS supportr.Use(middleware.CORSMiddleware)// Require HTMX for routesr.Route("/fragments", func(r *router.Router) { r.Use(middleware.RequireHTMX) r.GET("/user-card", UserCardFragment)})Getting the HTTP Handler
Convert the router to a standard http.Handler:
r := router.New()// ... register routes ...// Get the http.Handlerhandler := r.Handler()// Use with http.ListenAndServehttp.ListenAndServe(":8080", handler)// Or with desktop.Newapp := desktop.New(handler, config)Next Steps
- Templ Templates - Learn the templating syntax
- HTMX Integration - HTMX patterns and best practices
- Testing - Test your handlers
