The second path · Go application integration · v1.0.0-beta.2

Build the application around the templates.

The first tutorial builds a complete small site. This path slows down at the application boundary: how a Go router chooses a handler, how a handler prepares typed data, why rendering is buffered, where headers and middleware belong, and how a second page fits without turning templates into a framework.

Start here second: complete or skim the first site tutorial before this guide. I will explain the Go application shape, but I will not try to replace a core Go or net/http course.

The request path

Draw the boundary before adding code

A Sandwich Hime application has two different moments. During development, himesan generate turns reviewed .sando source into Go. During a request, your already-compiled Go program selects data and calls a generated component.

  1. ServeMuxmatches method and path
  2. handlervalidates request and loads data
  3. views.Pageassembles typed components
  4. sando.Renderwrites the response body

The template should not discover the current request, query a database, choose a status code, or mutate a cookie. Give it the data required to describe one page. Keep request and infrastructure decisions in ordinary Go where their lifetime and errors remain visible.

Routing

Let net/http decide which work exists

I like a small application type because its dependencies are explicit and its methods become handlers. Create internal/app/app.go:

package app

import (
    "bytes"
    "log/slog"
    "net/http"
    "strings"

    "example.com/trail/internal/views"
    "gamertan.com/sandwich-hime/sando"
)

type App struct {
    logger *slog.Logger
}

func New(logger *slog.Logger) *App {
    return &App{logger: logger}
}

func (a *App) Routes() http.Handler {
    mux := http.NewServeMux()
    mux.HandleFunc("GET /{$}", a.home)
    mux.HandleFunc("GET /about", a.about)
    return a.securityHeaders(mux)
}

GET /{$} means exactly the root path on current Go releases; it does not accidentally become a catch-all. GET /about gives the second page its own handler. The router knows HTTP method and path. It does not know how either page is rendered.

Routes returns http.Handler, so main, tests, another middleware layer, or a development supervisor can all use the same application without learning about its templates.

Handlers and page data

Turn request data into a typed page

The home handler may read a query parameter because that is request work. It then builds the same typed views introduced in the first tutorial:

func (a *App) home(w http.ResponseWriter, r *http.Request) {
    visitor := strings.TrimSpace(r.URL.Query().Get("name"))
    if visitor == "" {
        visitor = "traveler"
    }

    content := views.Home(views.HomeView{
        Visitor: visitor,
        Links: []views.Link{
            {Label: "Home", URL: "/"},
            {Label: "About", URL: "/about"},
        },
    })
    page := views.Layout(views.LayoutView{
        Title: "Hello from the mountain",
        Body:  content,
    })

    a.render(w, r, http.StatusOK, page)
}

The handler owns the default value and the navigation destinations. Home owns the home-page markup. Layout owns the document shell. Their nesting is an ordinary typed call: the handler can see exactly what page it is assembling.

In a larger application, the handler might call a service or repository before constructing HomeView. Pass the resulting display data into the component; do not pass a database handle or a whole application container into the template.

The response seam

Buffer once so errors remain honest

Put the shared response behavior in one helper:

func (a *App) render(
    w http.ResponseWriter,
    r *http.Request,
    status int,
    component sando.Component,
) {
    var output bytes.Buffer
    if err := sando.Render(r.Context(), &output, component); err != nil {
        a.logger.Error("render page", "path", r.URL.Path, "error", err)
        http.Error(w, "could not render page", http.StatusInternalServerError)
        return
    }

    w.Header().Set("Content-Type", "text/html; charset=utf-8")
    w.Header().Set("Cache-Control", "no-store")
    w.WriteHeader(status)
    if _, err := w.Write(output.Bytes()); err != nil {
        a.logger.Warn("write page", "path", r.URL.Path, "error", err)
    }
}

HTTP commits its status and headers when response bytes begin. Rendering into a buffer first means a component error can still produce a clean 500 instead of a half-written page carrying 200 OK. The tradeoff is one response-sized allocation; streaming can be an explicit application decision when a page is large enough to justify different failure semantics.

The request context flows through sando.Render. Handwritten components may use it for cancellation, but generated templates remain focused on their typed inputs.

Shared HTTP policy

Wrap the router without teaching templates about headers

Headers that apply to many routes belong in middleware or another response-policy layer:

func (a *App) securityHeaders(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("X-Content-Type-Options", "nosniff")
        w.Header().Set("Referrer-Policy", "no-referrer")
        w.Header().Set("Content-Security-Policy",
            "default-src 'none'; style-src 'self'; frame-ancestors 'none'")
        next.ServeHTTP(w, r)
    })
}

This policy is an example, not a universal CSP. If your page uses inline styles, images, forms, scripts, or another origin, design and test a policy for that application. Sandwich Hime can help keep values in supported HTML contexts; it cannot choose your browser policy.

Authentication, request IDs, access logs, compression, panic recovery, and rate limits fit the same wrapping model. Keep middleware ordered and test the behavior you depend on.

A second page

Reuse the shell without inventing inheritance

Create a small About component, then assemble it through the same layout and render helper:

<?sando go
package views

func About()
?>
<main id="main">
  <h1>About this trail</h1>
  <p>The router chose this page. The layout still owns the document.</p>
</main>

func (a *App) about(w http.ResponseWriter, r *http.Request) {
    page := views.Layout(views.LayoutView{
        Title: "About the trail",
        Body:  views.About(),
    })
    a.render(w, r, http.StatusOK, page)
}

The first half belongs in about.sando; the handler belongs in ordinary Go. They are shown together to make the seam visible. Generate the new component, then the Go compiler checks that the handler calls its real typed API.

If several handlers repeat the same navigation or account summary, create a typed page-data builder in Go. If several templates repeat the same markup, create another component. Those are different kinds of repetition and do not need one magical abstraction.

Application evidence

Test from the router inward

httptest can exercise the same handler returned to production:

package app_test

import (
    "io"
    "log/slog"
    "net/http"
    "net/http/httptest"
    "strings"
    "testing"

    "example.com/trail/internal/app"
)

func TestRoutesRenderTypedPages(t *testing.T) {
    handler := app.New(slog.New(slog.NewTextHandler(io.Discard, nil))).Routes()

    tests := []struct {
        target string
        status int
        marker string
    }{
        {"/?name=%3Cscript%3E", http.StatusOK, "&lt;script&gt;"},
        {"/about", http.StatusOK, "About this trail"},
        {"/missing", http.StatusNotFound, "404 page not found"},
    }

    for _, test := range tests {
        request := httptest.NewRequest(http.MethodGet, test.target, nil)
        response := httptest.NewRecorder()
        handler.ServeHTTP(response, request)

        if response.Code != test.status {
            t.Fatalf("%s status = %d", test.target, response.Code)
        }
        if !strings.Contains(response.Body.String(), test.marker) {
            t.Errorf("%s body missing %q", test.target, test.marker)
        }
        if got := response.Header().Get("X-Content-Type-Options"); got != "nosniff" {
            t.Errorf("%s nosniff = %q", test.target, got)
        }
    }
}

This checks routing, dynamic escaping, status behavior, and middleware together. Keep focused component tests for security-sensitive values and focused service tests for data rules; not every defect needs a browser-sized test.

Run himesan check internal/views before go test ./.... The first command checks template validity and generated freshness without writing. The second type-checks the generated API and exercises your application.

When the site grows

Keep each responsibility near its failure

Router
HTTP method, path, and which handler owns the request.
Middleware
Cross-cutting request and response policy such as identity, logging, headers, and recovery.
Handler
Input validation, authorization, service calls, status choice, and typed page assembly.
Service/repository
Business rules, transactions, external systems, and durable data.
.sando component
Visible markup, display decisions, typed composition, and contextual output.
Render helper
The deliberate seam between component errors and committed HTTP responses.

This is one calm Go application shape, not a Sandwich Hime framework contract. Replace the router, introduce a service layer, stream a response, or choose different middleware when your application earns it. The template engine still has one job: compile visible, typed templates into ordinary Go components.