Accepting the Request
copper.New() builds the app container, and app.Start(server) runs chttp.Server, which listens on the configured port (5901 in generated projects). Every request that arrives is handled by a single http.Handler assembled at startup in pkg/app/handler.go:
pkg/app/handler.go
Matching the Route
Copper collects every route from every router and sorts them automatically, so the most specific route wins regardless of declaration order. To learn how matching works, see Routing.Running the Middleware
The matched request then passes through the middleware chain, from the outside in:- Panic recovery, which Copper installs automatically. If anything below panics, the error and stack are logged and the client receives a 500.
- Global middlewares, in the order they are listed. In a generated project that means a request ID is placed in the context,
csql.TxMiddlewareopens a database transaction for mutating requests, and the request logger starts its timer. - Route middlewares, which run only for the route that declares them. Since they sit inside the global chain, they are the right place for concerns that apply to some endpoints but not others, such as auth on an admin route:
Handling the Request
Your handler is a plainhttp.HandlerFunc on a router struct. It reads path parameters with chttp.URLParams, decodes and validates the body, and calls into your services. Anything downstream that touches the database through csql.Querier automatically participates in the request’s transaction, since the transaction travels in the request context. To learn more, see Transactions.
Writing the Response
The handler writes its response through one of Copper’s writers:WriteJSON for APIs, WriteHTML for Go templates, or the Inertia renderer for React pages. As the response is written, the middleware chain unwinds:
TxMiddlewarecommits or rolls back the request’s transaction based on the response status, and anyOnCommitcallbacks run once the commit succeeds. The full rules live in Transactions.- The request logger records the method, path, status, and duration, and emits the built-in HTTP metrics.
Where Inertia Fits
An Inertia page render is an ordinary handler that ends ininertia.Render instead of WriteJSON, and the lifecycle above applies unchanged. The only difference is the response format, which the renderer picks per request:
- On a first visit, it renders the full HTML shell (
layouts/main.html), embeds the page component name and props as JSON, and the Vite render func injects the right script tags. React boots and takes over. - On subsequent navigations, Inertia’s client sends the
X-Inertiaheader, and the renderer responds with a small JSON payload of the next page’s props instead of HTML. No full page load happens again.