Routing & Handlers
irgo uses a chi-based router with a simplified API designed for Datastar applications. Handlers return HTML via SSE rather than writing directly 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)Datastar SSE Handlers
Datastar handlers return error and use ctx.SSE()to stream HTML updates via Server-Sent Events:
// Page handlers return (string, error) for full page rendersfunc HomeHandler(ctx *router.Context) (string, error) { return renderer.Render(templates.HomePage())}// Datastar handlers return error and use SSEfunc UserHandler(ctx *router.Context) error { id := ctx.Param("id") user, err := db.GetUser(id) if err != nil { return ctx.NotFound("User not found") } return ctx.SSE().PatchTempl(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}Datastar Detection & Signals
func handler(ctx *router.Context) error { // Check if request is from Datastar if ctx.IsDatastar() { // Handle as SSE response return ctx.SSE().PatchTempl(templates.UserCard(user)) } // Return full page for non-Datastar requests return ctx.HTML(renderer.Render(templates.UserPage(user)))}// Read signals from Datastar requestvar signals struct { Name string `json:"name"` Email string `json:"email"`}ctx.ReadSignals(&signals)Response Methods
// HTML responsesctx.HTML("<div>Hello</div>")ctx.HTMLStatus(201, "<div>Created</div>")// JSON responsesctx.JSON(user)ctx.JSONStatus(201, user)// Redirects (Datastar-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")SSE Response Methods
Control Datastar behavior with SSE response methods:
func handler(ctx *router.Context) error { sse := ctx.SSE() // Morph HTML into DOM (by element ID in the template) sse.PatchTempl(templates.UserCard(user)) // Update client-side signals sse.PatchSignals(map[string]any{"name": "", "loading": false}) // Remove elements from DOM sse.RemoveElements("#old-item") // Redirect to new URL sse.Redirect("/users/123") // Log to browser console sse.Console("info", "Update complete") return nil}Multiple Updates
With Datastar, you can call multiple PatchTempl methods in a single handler to update multiple elements. Each element is identified by its ID in the template.
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 Datastar requests (sets ctx.IsDatastar())r.Use(middleware.DatastarRequestMiddleware)// Wrap non-Datastar responses in layoutr.Use(middleware.LayoutWrapper(templates.Layout))// Prevent cachingr.Use(middleware.NoCacheMiddleware)// CORS supportr.Use(middleware.CORSMiddleware)// Require Datastar for routesr.Route("/api", func(r *router.Router) { r.Use(middleware.RequireDatastar) r.DSGet("/user-card", UserCardHandler)})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
- Datastar Integration - Datastar patterns and best practices
- Testing - Test your handlers
