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.
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.
ServeMuxmatches method and pathhandlervalidates request and loads dataviews.Pageassembles typed componentssando.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, "<script>"},
{"/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.
.sandocomponent- 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.