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