# App Lifecycle
Source: https://docs.gocopper.dev/architecture/app-lifecycle
Every Copper binary starts the same way: you create the app, then hand it something to run. Let's look at the `main.go` from a generated project:
```go cmd/app/main.go theme={null}
func main() {
app := copper.New()
server, err := InitServer(app)
if err != nil {
log.Fatalln("Failed to init server: ", err.Error())
}
app.Start(server)
}
```
`copper.New()` builds the **app container**, which parses command-line flags, loads config, and sets up the logger and lifecycle:
```go theme={null}
type App struct {
Lifecycle *clifecycle.Lifecycle
Config cconfig.Loader
Logger clogger.CoreLogger
}
```
### Runners
Anything with a `Run() error` method can be run by the app:
```go theme={null}
type Runner interface {
Run() error
}
```
`chttp.Server` and `csql.Migrator` both implement it, and you are free to implement it with your own types, such as a job worker, a queue consumer, or a one-off script.
### Run vs. Start
* **`app.Run(runners...)`** is for short-lived work. It runs each runner in order, then shuts down. This is how the migrate binary works: `app.Run(migrator)`.
* **`app.Start(runners...)`** is for long-lived work. It runs each runner, then blocks until the process receives `SIGINT` or `SIGTERM`, and shuts down gracefully. This is how the HTTP server runs.
If any runner returns an error, the app logs it and exits with code 1.
### Registering Cleanup with OnStop
You may register cleanup work, such as closing a database connection or flushing a client, using `OnStop`. Copper will run these functions during shutdown, each with a deadline context:
```go theme={null}
func NewClient(lc *clifecycle.Lifecycle) (*Client, error) {
conn, err := dial()
if err != nil {
return nil, err
}
lc.OnStop(func(ctx context.Context) error {
return conn.Close()
})
return &Client{conn: conn}, nil
}
```
### Running Background Goroutines
You should not use a bare `go` statement for long-running work, since the app has no way to wait for it during shutdown. Instead, use `Lifecycle.Go`. Goroutines started this way are tracked, awaited on shutdown, and recovered from panics, so a panic is logged with its stack trace instead of crashing the app:
```go theme={null}
lc.Go(func(ctx context.Context) {
err := telemetry.Upload(ctx, flightData)
if err != nil {
logger.Error("Failed to upload flight data", err)
}
})
```
### Graceful Shutdown
Shutdown happens in two phases, which is what makes it graceful:
1. **`Lifecycle.Shutdown()`** is a channel that closes when shutdown *begins*. Loops should select on it to exit.
2. **`Lifecycle.Context()`** is a context that stays valid *during* shutdown so that in-flight work can finish. It is only canceled at the very end.
A long-running worker uses both:
```go theme={null}
func (t *Tracker) Run() error {
t.lc.Go(func(ctx context.Context) {
ticker := time.NewTicker(10 * time.Second)
defer ticker.Stop()
for {
select {
case <-t.lc.Shutdown():
return
case <-ticker.C:
t.pingSatellites(ctx)
}
}
})
return nil
}
```
Shutdown waits up to 30 seconds for `Go` goroutines to finish before running `OnStop` funcs and exiting. Work that can't finish in time is abandoned, so keep background tasks incremental.
# Dependency Injection
Source: https://docs.gocopper.dev/architecture/dependency-injection
Copper uses [google/wire](https://github.com/google/wire) for dependency injection. Wire generates plain Go code at build time, so there is no runtime container and no reflection. If your dependency graph has a problem, you will find out with a compile error rather than a production surprise.
You will rarely write wire code by hand. `copper scaffold:pkg`, `scaffold:router`, and `scaffold:queries` maintain it for you, and `copper run` regenerates it on every build.
### Wire Modules
Each package declares what it provides in a `wire.go` file:
```go pkg/rockets/wire.go theme={null}
var WireModule = wire.NewSet(
LoadConfig,
wire.Struct(new(NewSvcParams), "*"),
NewSvc,
wire.Struct(new(NewRouterParams), "*"),
NewRouter,
)
```
The `wire.Struct(new(NewSvcParams), "*")` line tells wire to fill every field of the params struct from the dependency graph. This **params struct convention** is used throughout Copper:
```go theme={null}
type NewSvcParams struct {
Queries *Queries
Mission *missions.Svc
Logger clogger.Logger
}
func NewSvc(p NewSvcParams) *Svc {
return &Svc{queries: p.Queries, mission: p.Mission, logger: p.Logger}
}
```
Adding a dependency is a one-line struct change. You will not need to update constructor signatures throughout your codebase, or edit any wire code beyond the package's own module.
### Composing Modules
`pkg/app/wire.go` is the composition root for your packages. `copper scaffold:pkg` adds each new package's module to it, alongside a few app-level providers:
```go pkg/app/wire.go theme={null}
var WireModule = wire.NewSet(
MetricsRegistry,
ProvideLoggerHooks,
rockets.WireModule,
missions.WireModule,
)
```
Copper ships several wire modules that you may pull into your binaries as needed:
| Module | Provides |
| --------------------------- | ---------------------------------------------------------------------------------------------------- |
| `copper.WireModule` | `cconfig.Loader`, `*clifecycle.Lifecycle`, `clogger.Logger` |
| `chttp.WireModule` | HTTP server, JSON/HTML reader-writers, request logger middleware |
| `chttp.WireModuleEmptyHTML` | Empty HTML/static dir values to satisfy `chttp.WireModule` in API-only apps with no `web/` directory |
| `csql.WireModule` | DB connection, `csql.Querier`, migrator, transaction middleware |
| `cmetrics.WireModule` | `cmetrics.Metrics` (you provide the `*Registry`) |
### Binding Interfaces
If you would like to depend on an interface rather than a concrete type, add a `wire.Bind`:
```go theme={null}
var WireModule = wire.NewSet(
wire.Struct(new(NewSvcParams), "*"),
NewSvc,
wire.Bind(new(Telemetry), new(*SatelliteTelemetry)),
)
```
Any package may now ask for `Telemetry` and receive the satellite implementation. If you decide to swap implementations later, it is a one-line change.
### Regenerating
Wire output is generated into `wire_gen.go` files, which are checked in. `copper run` and `copper build` regenerate them automatically. If you would like to run the generator manually:
```
go run github.com/google/wire/cmd/wire ./...
```
Wire errors can look intimidating, but they almost always say one of two things: a type has **no provider** (a constructor or module is missing from a wire set) or has **two providers** (the same type is provided in two modules). Read the first error, find the type it names, and check your `WireModule`s for it.
# Request Lifecycle
Source: https://docs.gocopper.dev/architecture/request-lifecycle
Understanding how a request travels through a Copper application makes everything else in these docs feel less magical. This page follows one request from the socket to the response.
### 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`:
```go pkg/app/handler.go theme={null}
func NewHTTPHandler(p NewHTTPHandlerParams) http.Handler {
return chttp.NewHandler(chttp.NewHandlerParams{
Routers: []chttp.Router{
p.Rockets,
},
GlobalMiddlewares: []chttp.Middleware{
chttp.SetRequestIDInCtxMiddleware(),
p.DatabaseTxMW,
p.RequestLoggerMW,
},
Logger: p.Logger,
})
}
```
This is the one place where your application's routers and global middlewares come together. Since the handler is built by [dependency injection](/architecture/dependency-injection), adding a router means adding a field, nothing more.
### 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](/http/routing).
### Running the Middleware
The matched request then passes through the middleware chain, from the outside in:
1. **Panic recovery**, which Copper installs automatically. If anything below panics, the error and stack are logged and the client receives a 500.
2. **Global middlewares**, in the order they are listed. In a generated project that means a request ID is placed in the context, `csql.TxMiddleware` opens a database transaction for mutating requests, and the request logger starts its timer.
3. **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:
```go theme={null}
{
Path: "/admin/rockets",
Methods: []string{http.MethodPost},
Middlewares: []chttp.Middleware{ro.authMW},
Handler: ro.HandleDecommissionRocket,
},
```
Each middleware wraps the next, so a request runs panic recovery, then each global middleware in order, then each route middleware in order, then your handler; the same chain unwinds in reverse as the response is written. To learn more, including the built-in middlewares and how to write your own, see [Middleware](/http/middleware).
### Handling the Request
Your handler is a plain `http.HandlerFunc` on a router struct. It reads path parameters with `chttp.URLParams`, decodes and validates the body, and calls into your [services](/the-basics/packages). 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](/database/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:
* `TxMiddleware` commits or rolls back the request's transaction based on the response status, and any `OnCommit` callbacks run once the commit succeeds. The full rules live in [Transactions](/database/transactions).
* The request logger records the method, path, status, and duration, and emits the built-in [HTTP metrics](/digging-deeper/metrics).
### Where Inertia Fits
An Inertia page render is an ordinary handler that ends in `inertia.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](/frontend/vite) injects the right script tags. React boots and takes over.
* On **subsequent navigations**, Inertia's client sends the `X-Inertia` header, and the renderer responds with a small JSON payload of the next page's props instead of HTML. No full page load happens again.
Either way it is the same route, the same middleware chain, and the same transaction semantics. To learn more, see [Inertia](/frontend/inertia).
### Next
Now that you can trace a request end to end, you may want to read [App Lifecycle](/architecture/app-lifecycle) for what happens at startup and shutdown, or [Dependency Injection](/architecture/dependency-injection) for how the handler graph is assembled.
# Migrations
Source: https://docs.gocopper.dev/database/migrations
Migrations are plain SQL files in the `migrations/` directory, run with [sql-migrate](https://github.com/rubenv/sql-migrate). Since they are embedded into your binary, a deployed application carries its own schema history.
### Writing Migrations
Migration files are named sequentially with a short description, such as `0001_initial.sql` and `0002_create_rockets.sql`. Each file has an up and a down section:
```sql migrations/0002_create_rockets.sql theme={null}
-- +migrate Up
CREATE TABLE rockets (
id TEXT PRIMARY KEY,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
name TEXT NOT NULL,
fuel BIGINT NOT NULL DEFAULT 0
);
CREATE INDEX idx_rockets_name ON rockets (name);
-- +migrate Down
DROP TABLE IF EXISTS rockets;
```
### Running Migrations
During development, you rarely need to think about migrations since `copper run` applies any pending ones before starting your application. If you would like to skip this, pass `-migrate=false`.
You may also run migrations on their own:
```
copper migrate
```
To roll back, pass the direction:
```
copper migrate -direction=down
```
`down` reverts only the most recent migration. You may run it again to step back further.
### Migrations in Production
`copper build` compiles a standalone migrator alongside your app:
```
copper build
./build/migrate.out -config config/prod.toml # apply pending migrations
./build/app.out -config config/prod.toml # start the app
```
Run the migrator as a deploy step before starting (or restarting) the app.
To roll back the most recent migration, set the direction with `-set`:
```
./build/migrate.out -config config/prod.toml -set 'csql.migrations.direction="down"'
```
The value is parsed as TOML, so the inner quotes matter. All of the `[csql.migrations]` keys are listed in the [Configuration Reference](/reference/configuration).
# Models
Source: https://docs.gocopper.dev/database/models
Copper models are plain Go structs. There is no ORM and no base class to extend; a model is simply a struct whose fields are tagged with the columns they map to.
### Defining a Model
Models conventionally live in your package's `models.go`. Columns are mapped to fields using `db:` tags, and you may add `json:` tags alongside them when the same struct shapes your API responses or Inertia props:
```go pkg/rockets/models.go theme={null}
type Rocket struct {
ID int64 `db:"id,readonly" json:"id"`
CreatedAt time.Time `db:"created_at,readonly" json:"createdAt"`
Name string `db:"name" json:"name"`
Fuel int64 `db:"fuel" json:"fuel"`
Note *string `db:"note" json:"note,omitempty"`
}
```
The `db:` tags are used both when scanning rows in [Queries](/database/queries) and when generating SQL fragments with [Query Builder](/database/query-builder).
### Nullable Columns
Pointer fields map to nullable columns. A `NULL` value scans as `nil`, and writing `nil` stores a `NULL`. If you would like to build pointers inline, the `cvars.Ptr` helper saves you a temporary variable:
```go theme={null}
rocket.Note = cvars.Ptr("engine check complete")
```
### The readonly Modifier
The `,readonly` modifier marks columns that are written once and never updated, such as `id` and `created_at`. Readonly columns are included when inserting but excluded when updating, which is enforced by the [Query Builder](/database/query-builder) `SetColumns` and `SetValues` functions.
### Tag Rules
* Fields without a `db:` tag, tagged `db:"-"`, or unexported are skipped.
* Embedded structs are flattened; their tagged fields are included recursively.
* `db:"col,readonly"` includes the column in inserts but excludes it from updates, as described above.
### Custom Column Types
Since scanning is built on `database/sql`, any type that implements `driver.Valuer` and `sql.Scanner` may be used as a column type. This lets a domain type validate and format itself while being stored as a simple column:
```go theme={null}
type LaunchCode string
func (c LaunchCode) Value() (driver.Value, error) {
return string(c), nil
}
func (c *LaunchCode) Scan(src any) error {
s, ok := src.(string)
if !ok {
return fmt.Errorf("cannot scan %T into LaunchCode", src)
}
*c = LaunchCode(s)
return nil
}
```
### Next
Now that you have a model, you may want to read and write it with [Queries](/database/queries), or generate the SQL fragments from its tags with [Query Builder](/database/query-builder).
# Queries
Source: https://docs.gocopper.dev/database/queries
Copper's `csql` package provides a thin, productive layer over `database/sql`. Queries are parameterized and scanned into plain structs, so there is no ORM to learn and no query language beyond SQL itself.
### The Queries Struct
Each package that talks to the database keeps its queries in a `Queries` struct. You may scaffold one using the CLI:
```
copper scaffold:queries rockets
```
This creates `pkg/rockets/queries.go` and adds it to the package's wire module:
```go pkg/rockets/queries.go theme={null}
func NewQueries(querier csql.Querier) *Queries {
return &Queries{querier: querier}
}
type Queries struct {
querier csql.Querier
}
```
### Models
Rows are scanned into plain structs whose `db:` tags map columns to fields. The examples on this page use the following model; to learn more about defining models, tag rules, and custom column types, see [Models](/database/models):
```go pkg/rockets/models.go theme={null}
type Rocket struct {
ID int64 `db:"id,readonly"`
CreatedAt time.Time `db:"created_at,readonly"`
Name string `db:"name"`
Fuel int64 `db:"fuel"`
Note *string `db:"note"` // nullable column
}
```
### Reading Rows
The `Get` method scans a single row into a struct, while `Select` scans multiple rows into a slice. Rather than writing `SELECT *` or a hand-maintained column list, render the columns from the model's `db:` tags with [`qb.Columns`](/database/query-builder):
```go theme={null}
func (q *Queries) GetRocket(ctx context.Context, id int64) (*Rocket, error) {
var rocket Rocket
query := fmt.Sprintf("SELECT %s FROM rockets WHERE id = ?", qb.Columns(Rocket{}))
err := q.querier.Get(ctx, &rocket, query, id)
if err != nil {
return nil, err
}
return &rocket, nil
}
func (q *Queries) ListRockets(ctx context.Context) ([]Rocket, error) {
var rockets []Rocket
query := fmt.Sprintf("SELECT %s FROM rockets ORDER BY created_at DESC", qb.Columns(Rocket{}))
err := q.querier.Select(ctx, &rockets, query)
if err != nil {
return nil, err
}
return rockets, nil
}
```
You should always write `?` placeholders, regardless of your database. Copper will automatically rebind them to your dialect's native style, such as `$1` for Postgres, before executing the query.
### Writing Rows
`Exec` runs INSERT, UPDATE, and DELETE statements:
```go theme={null}
func (q *Queries) RefuelRocket(ctx context.Context, id int64, fuel int64) error {
result, err := q.querier.Exec(ctx,
"UPDATE rockets SET fuel = ? WHERE id = ?", fuel, id)
if err != nil {
return err
}
n, err := result.RowsAffected()
if err != nil {
return err
}
if n == 0 {
return ErrRocketNotFound
}
return nil
}
```
### Handling Missing Rows
`Get` returns `sql.ErrNoRows` when nothing matches. It is common to alias it as a package sentinel so that callers do not need to import `database/sql`:
```go pkg/rockets/models.go theme={null}
var ErrRocketNotFound = sql.ErrNoRows
```
```go theme={null}
rocket, err := ro.queries.GetRocket(ctx, id)
if errors.Is(err, rockets.ErrRocketNotFound) {
// respond with 404
}
```
### IN Queries
To pass a slice into an `IN (?)` clause, use `WithIn()`:
```go theme={null}
var rockets []Rocket
query := fmt.Sprintf("SELECT %s FROM rockets WHERE id IN (?)", qb.Columns(Rocket{}))
err := q.querier.WithIn().Select(ctx, &rockets, query, []int64{101, 102})
```
Copper expands the single `?` into one placeholder per element of the slice.
### Connections and Drivers
Copper opens the connection from your config and closes it on shutdown:
```toml config/dev.toml theme={null}
[csql]
dialect = "pgx"
dsn = "postgresql://postgres:dev@127.0.0.1/postgres?sslmode=disable"
```
The database driver is blank-imported by your application rather than by Copper. `copper create` sets this up for you in `cmd/app/wire.go`:
```go theme={null}
import (
_ "github.com/jackc/pgx/v5/stdlib"
)
```
The pool defaults to 25 open connections, 25 idle connections, and a 5-minute connection lifetime. If you would like to tune these, set `max_open_connections`, `max_idle_connections`, and `conn_max_lifetime_mins` under `[csql]`. All of the available keys are listed in the [Configuration Reference](/reference/configuration).
### Next
Now that you can read and write rows, you may want to learn how Copper wraps mutating requests in automatic [Transactions](/database/transactions), or let [Query Builder](/database/query-builder) write your column lists for you.
# Query Builder
Source: https://docs.gocopper.dev/database/query-builder
Hand-written column lists are tedious to maintain and tend to drift out of sync with your models. The `csql/qb` package derives SQL fragments from your models' `db:` tags, so your queries stay correct as the struct changes.
Throughout this page, let's assume we have the following model:
```go theme={null}
type Rocket struct {
ID string `db:"id,readonly"`
CreatedAt time.Time `db:"created_at,readonly"`
Name string `db:"name"`
Fuel int64 `db:"fuel"`
}
```
### Inserts
`qb.Columns` renders the column list, `qb.ValuePlaceholders` the matching `?`s, and `qb.Values` the arguments:
```go theme={null}
func (q *Queries) InsertRocket(ctx context.Context, rocket Rocket) error {
query := fmt.Sprintf("INSERT INTO rockets (%s) VALUES (%s)",
qb.Columns(rocket), qb.ValuePlaceholders(rocket))
_, err := q.querier.Exec(ctx, query, qb.Values(rocket)...)
return err
}
```
This renders `INSERT INTO rockets (id, created_at, name, fuel) VALUES (?, ?, ?, ?)`.
### Selects
`qb.Columns` also works on a zero value, which is handy for SELECT statements:
```go theme={null}
func (q *Queries) GetRocket(ctx context.Context, id int64) (*Rocket, error) {
var rocket Rocket
query := fmt.Sprintf("SELECT %s FROM rockets WHERE id = ?", qb.Columns(Rocket{}))
err := q.querier.Get(ctx, &rocket, query, id)
if err != nil {
return nil, err
}
return &rocket, nil
}
```
For joined queries, you may pass a table alias; `qb.Columns(Rocket{}, "r")` renders `r.id, r.created_at, r.name, r.fuel`.
### Updates and `readonly`
`qb.SetColumns` and `qb.SetValues` render `SET` clauses, and this is where the `,readonly` tag modifier earns its keep. Columns marked `readonly`, such as `id` and `created_at`, are included in inserts but excluded from updates:
```go theme={null}
func (q *Queries) UpdateRocket(ctx context.Context, rocket Rocket) error {
query := fmt.Sprintf("UPDATE rockets SET %s WHERE id = ?", qb.SetColumns(rocket))
_, err := q.querier.Exec(ctx, query, append(qb.SetValues(rocket), rocket.ID)...)
return err
}
```
This renders `UPDATE rockets SET name = ?, fuel = ? WHERE id = ?`. The readonly columns never appear.
### Upserts
`qb.ValuesAndSetValues` returns the insert values followed by the update values, which lines up exactly with an upsert:
```go theme={null}
query := fmt.Sprintf(
"INSERT INTO rockets (%s) VALUES (%s) ON CONFLICT (id) DO UPDATE SET %s",
qb.Columns(rocket), qb.ValuePlaceholders(rocket), qb.SetColumns(rocket))
_, err := q.querier.Exec(ctx, query, qb.ValuesAndSetValues(rocket)...)
```
All of the functions above follow the same tag rules: untagged, `db:"-"`, and unexported fields are skipped, while embedded structs are flattened recursively. To learn more, see [Models](/database/models).
# Transactions
Source: https://docs.gocopper.dev/database/transactions
In Copper, transactions travel through `context.Context`. When a transaction is active, every `Get`, `Select`, and `Exec` that receives that context automatically runs inside it. Your queries and services do not need a `tx` parameter, and the same code works identically inside and outside a transaction.
### Automatic Transactions per Request
Apps scaffolded with a database include `csql.TxMiddleware` as a global middleware. It wraps every **mutating** request (POST, PUT, PATCH, DELETE) in a transaction:
* On a 2xx or 3xx response, the transaction commits.
* On a 4xx or 5xx response, it rolls back.
* GET, HEAD, and OPTIONS requests are not wrapped; reads auto-commit against the pool.
As a result, a handler that writes three rows and then fails is atomic by default, without any transaction plumbing in your handlers.
If the commit itself fails on a 2xx response, the middleware will respond with a 500 instead, so a client never sees a success status for data that was not committed.
### Explicit Transactions
Outside of HTTP handlers, such as background workers or CLI tasks, you may manage transactions explicitly using `InTx`:
```go theme={null}
err := s.querier.InTx(ctx, func(ctx context.Context) error {
if err := s.queries.DeductFuel(ctx, rocketID, amount); err != nil {
return err
}
return s.queries.RecordBurn(ctx, rocketID, amount)
})
```
Return `nil` to commit, or an error to roll everything back. Panics are caught by a deferred rollback, so a transaction is never left open.
`InReadOnlyTx` works the same way but always rolls back, which is useful for running multiple reads against one consistent snapshot.
### Running After the Commit
Some side effects, such as publishing an event or sending an email, should only happen if the transaction commits. You may register them using `OnCommit`:
```go theme={null}
func (s *Svc) LaunchRocket(ctx context.Context, id int64) error {
launch, err := s.queries.InsertLaunch(ctx, id)
if err != nil {
return err
}
event := LaunchedEvent{LaunchID: launch.ID, RocketID: id}
return s.querier.OnCommit(ctx, func(ctx context.Context) error {
return s.pubsub.Publish(ctx, TopicRocketLaunched, event)
})
}
```
If the transaction rolls back, the callback is discarded. Since callbacks run in the background with a 30-second timeout, you should snapshot the data you need (like `event` above) before registering the callback rather than re-reading it inside.
If there is no transaction in the context, `OnCommit` runs the callback immediately in the background, so the same service code works with or without an ambient transaction.
### Escaping the Transaction
Occasionally a write should persist even if the surrounding transaction rolls back, such as an audit log or a failure record. You may use `csql.CtxWithoutTx` to run a query outside the ambient transaction:
```go theme={null}
// Recorded even if the request's transaction rolls back
_ = s.queries.RecordLaunchAttempt(csql.CtxWithoutTx(ctx), rocketID)
```
All other context values, such as deadlines, tracing, and auth, are preserved; only the transaction is removed.
### Dry Runs
Because the request transaction commits or rolls back based on your response, you get dry-run support almost for free: do the real work, then roll back before responding.
```go theme={null}
func (ro *Router) HandleLaunchRocket(w http.ResponseWriter, r *http.Request) {
var (
ctx = r.Context()
id = chttp.URLParams(r)["id"]
)
launch, err := ro.rockets.Launch(ctx, id)
if err != nil {
ro.logger.Error("Failed to launch rocket", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
if r.URL.Query().Get("dry_run") == "true" {
_ = ro.querier.RollbackTx(ctx)
}
ro.rw.WriteJSON(w, chttp.WriteJSONParams{Data: launch})
}
```
The full code path runs, including validations, queries, and computed results, but nothing is persisted, and any `OnCommit` callbacks are discarded with the rollback.
### Next
Next, learn how to define and evolve your schema with [Migrations](/database/migrations).
# Background Work
Source: https://docs.gocopper.dev/digging-deeper/background-work
Not everything happens during a request. This guide covers the patterns for running work in the background, from a quick goroutine to a dedicated worker binary.
### Managed Goroutines
You should avoid starting a bare `go func()` in a Copper application. Instead, use `Lifecycle.Go`; Copper recovers panics for you and will automatically wait for the goroutine to finish during graceful shutdown:
```go theme={null}
s.lifecycle.Go(func(ctx context.Context) {
s.processTelemetry(ctx, batch)
})
```
### Long-Running Workers
For a worker loop, combine `Lifecycle.Go` with `Lifecycle.Shutdown()`, which closes when the app begins shutting down:
```go pkg/telemetry/poller.go theme={null}
func (p *Poller) Start() {
p.lifecycle.Go(func(ctx context.Context) {
ticker := time.NewTicker(30 * time.Second)
defer ticker.Stop()
for {
select {
case <-p.lifecycle.Shutdown():
return
case <-ticker.C:
if err := p.pollRockets(ctx); err != nil {
p.logger.Error("Failed to poll rocket telemetry", err)
}
}
}
})
}
```
To learn more about the shutdown mechanics, see [App Lifecycle](/architecture/app-lifecycle).
### A Dedicated Worker Binary
When background work outgrows the web process, you may give it its own binary. Add a `cmd/jobs` directory with the same `main.go`/`wire.go` shape as `cmd/app`, wire the same `app.WireModule`, and start a runner instead of the HTTP server:
```go cmd/jobs/main.go theme={null}
func main() {
app := copper.New()
runner, err := InitRunner(app)
if err != nil {
app.Logger.Error("Failed to init jobs runner", err)
os.Exit(1)
}
app.Start(runner)
}
```
A runner is anything with a `Run() error` method (the `copper.Runner` interface). Both binaries share one dependency graph, and `copper build` compiles every target under `cmd/`, so deploying the worker is simply deploying another binary from the same codebase.
### Work Tied to a Transaction
Sometimes background work should only happen if the surrounding database transaction commits, such as sending an email or publishing an event. In that case, register the work with `querier.OnCommit` instead of acting immediately; if the transaction rolls back, the callback never runs. To learn more, see [Transactions](/database/transactions).
### Reacting to Events
When one action should trigger several reactions, you may publish an event and let interested packages subscribe. See [Pub/Sub](/digging-deeper/pubsub).
### Scheduled Work
The simplest cron in a Copper app is an HTTP route. Add an `/internal/...` route that performs the work, and have your scheduler (crontab, Cloud Scheduler, Kubernetes CronJob) POST to it:
```go theme={null}
{
Path: "/internal/rebalance-fuel",
Methods: []string{http.MethodPost},
Handler: ro.HandleRebalanceFuel,
},
```
```
*/15 * * * * curl -X POST http://localhost:5901/internal/rebalance-fuel
```
This approach gives you request logging, metrics, and manual triggering for free, since running the job by hand is just a `curl`. However, you should keep `/internal/` routes unexposed at the network layer, since the application does not authenticate them.
# Deployment
Source: https://docs.gocopper.dev/digging-deeper/deployment
A Copper application deploys as a single binary, with templates, static assets, and migrations embedded at build time.
### Building
To compile your application for production, run the `build` command:
```
copper build
```
This regenerates wire code, embeds your frontend's built assets, and compiles every target under `cmd/` into `build/`:
```
build/app.out
build/migrate.out
```
If your application has a `web/` frontend, you should run its production build first, since `copper build` embeds the build output but does not run the frontend build itself:
```
cd web && bun run build
```
You may use the `--only` flag to build specific targets:
```
copper build --only=app,migrate
```
### Running in Production
Run migrations first, then start the app with your production config:
```
./build/migrate.out -config config/prod.toml
./build/app.out -config config/prod.toml
```
Copper shuts down gracefully on `SIGINT` and `SIGTERM`: in-flight requests finish, background goroutines are waited on, and cleanup hooks run, all within a 30-second limit. You should point your process manager's stop signal at `SIGTERM` and allow at least that long.
### Production Configuration
You should keep `config/prod.toml` free of secrets. Since config files are templates, you may pull secrets from the environment or a secrets manager at startup:
```toml config/prod.toml theme={null}
extends = ["base.toml"]
[csql]
dsn = "{{ .EnvVars.DATABASE_URL }}"
[rockets]
telemetry_api_key = "{{ exec "gcloud secrets versions access latest --secret=TELEMETRY_API_KEY" }}"
[clogger]
format = "json"
level_filter = ["info", "warn", "error"]
```
JSON logs with a level filter play well with log aggregators. To learn more, see [Configuration](/the-basics/configuration) and [Logging](/the-basics/logging).
### Docker
Copper apps containerize well, since the whole application is a single binary plus a config directory. To learn how to write a multi-stage Dockerfile with proper layer caching, see [Docker](/digging-deeper/docker).
### One-Off Overrides
You may override any config value at the command line without editing files:
```
./build/app.out -config config/prod.toml -set "chttp.port=8080"
```
# Docker
Source: https://docs.gocopper.dev/digging-deeper/docker
Since a Copper application compiles to a single binary, it fits Docker naturally: build the frontend in a JavaScript image, compile the app in a Go image, and run it in a minimal image that contains little more than your binaries and config. Keeping the stages separate also gives you precise layer caching, so a frontend-only change does not rebuild your Go code, and vice versa.
### The Dockerfile
The following multi-stage build covers the default GRIP application:
```docker Dockerfile theme={null}
# Stage 1: build the frontend
FROM oven/bun:1 AS web
WORKDIR /src/web
COPY web/package.json web/bun.lock ./
RUN bun install --frozen-lockfile
COPY web ./
RUN bun run build
# Stage 2: compile the app
FROM golang:1.24-alpine AS build
RUN go install github.com/gocopper/cli/cmd/copper@v1.4.0
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
COPY --from=web /src/web/build ./web/build
RUN copper build --only=app,migrate
# Stage 3: the runtime image
FROM alpine:3.21
RUN apk add --no-cache ca-certificates
WORKDIR /app
COPY --from=build /src/build ./build
COPY --from=build /src/config ./config
EXPOSE 5901
CMD ["./build/app.out", "-config", "config/prod.toml"]
```
The frontend stage runs the Vite production build, and its output is copied into the Go stage before `copper build` embeds it. This ordering matters: `copper build` embeds whatever is in `web/build` but does not run the frontend build itself, so the Go image never needs a JavaScript toolchain. If you use npm instead of bun, swap the first stage for a `node` image and copy `package-lock.json` in place of `bun.lock`.
### Caching Dependencies
Each stage copies its dependency manifests and installs before copying the rest of the source. Docker caches those layers, so day-to-day builds skip `bun install` and `go mod download` entirely unless a lockfile changes. You are free to tighten this further with a `.dockerignore`:
```text .dockerignore theme={null}
build/
web/build/
web/node_modules/
config/local.toml
*.db
.git/
```
Always exclude `config/local.toml` and any other secret-bearing files from the image. Production secrets should reach the app through the environment or a secrets manager, as described in [Deployment](/digging-deeper/deployment).
### Running Migrations
The image includes `migrate.out`, so you may run migrations as a release step before starting the new version:
```
docker run --rm my-app ./build/migrate.out -config config/prod.toml
```
Orchestrators offer a natural home for this: an init container in Kubernetes, a release command in your deploy tool, or simply a step in your pipeline. To learn more, see [Migrations](/database/migrations).
### API-Only Applications
If your application was created with `-frontend none`, drop the first stage and the `COPY --from=web` line; the rest of the Dockerfile works as-is.
If you use the `sqlite3` storage, the binary depends on cgo. Build with `CGO_ENABLED=1` and a C toolchain (`apk add gcc musl-dev` on alpine), and keep the builder and runtime libc in sync. The Postgres and MySQL drivers are pure Go, so the default stack needs none of this.
### Graceful Shutdown
Copper shuts down cleanly on `SIGTERM`, which is what `docker stop` and most orchestrators send. Since in-flight requests, background goroutines, and cleanup hooks are given up to 30 seconds, you should allow at least that long before the platform escalates to `SIGKILL`: for example, `docker stop --timeout 35` or `terminationGracePeriodSeconds: 35` in Kubernetes.
### Next
Now that your application is containerized, you may want to review [Deployment](/digging-deeper/deployment) for production configuration and secrets, or [Metrics](/digging-deeper/metrics) to monitor it once it is running.
# Metrics
Source: https://docs.gocopper.dev/digging-deeper/metrics
Copper's `cmetrics` package provides Prometheus metrics with a declare-then-use model: register your counters and histograms up front, then emit them from anywhere. The `cprometheus` package (from [gocopper/pkg](https://github.com/gocopper/pkg)) exposes them over HTTP.
### Declaring Metrics
To declare your application's metrics, provide a `*cmetrics.Registry` and add `cmetrics.WireModule` to your wire set:
```go pkg/app/metrics.go theme={null}
func MetricsRegistry() *cmetrics.Registry {
return cmetrics.NewRegistry(cmetrics.NewRegistryParams{
Counters: []cmetrics.Counter{
{Name: "rocket_launches_total", Labels: []string{"status"}},
},
Histograms: []cmetrics.Histogram{
{Name: "launch_duration_seconds", Labels: []string{"rocket"}, Buckets: []float64{1, 5, 15, 60}},
},
})
}
```
Projects scaffolded with `copper create` already include this file and the wiring, so you only need to add your metrics to the registry.
### Emitting Metrics
To emit a metric, inject `cmetrics.Metrics` and reference the metric by name:
```go theme={null}
s.metrics.CounterInc("rocket_launches_total", map[string]string{"status": "success"})
s.metrics.HistogramObserve("launch_duration_seconds", map[string]string{"rocket": rocket.Name}, dur.Seconds())
```
Emitting an unregistered metric name (or a mismatched label set) logs a warning and does nothing; it will never panic or return an error.
### Built-in HTTP Metrics
Every registry automatically includes two built-in metrics, which are emitted by `chttp.RequestLoggerMiddleware` (wired in by default):
* `http_requests_total{status_code, path}`
* `http_request_duration_seconds{status_code, path}`
The `path` label is the route template (`/api/rockets/{id}`) rather than the raw URL, so cardinality stays manageable.
### Exposing the Metrics Endpoint
To expose your metrics over HTTP, add `cprometheus.WireModule`, register `*cprometheus.Router` in your handler, and enable the endpoint in your config:
```toml theme={null}
[cprometheus]
http_enabled = true
http_path = "/internal/metrics" # default
```
The endpoint is disabled by default, so you may enable it only in the environments that Prometheus scrapes.
# Pub/Sub
Source: https://docs.gocopper.dev/digging-deeper/pubsub
The `cpubsub` package (from [gocopper/pkg](https://github.com/gocopper/pkg)) allows your packages to publish events without knowing who consumes them. Two backends are included: an in-process local backend, and a Redis backend for when multiple processes need to hear about events. To get started, install the package:
```
go get github.com/gocopper/pkg/cpubsub
```
### The PubSub Interface
Both backends implement the same small interface:
```go theme={null}
type PubSub interface {
Subscribe(ctx context.Context, topic string, handler Handler) error
Publish(ctx context.Context, topic string, payload any) error
}
```
Choose a backend by adding the corresponding wire module to your app:
```go theme={null}
cpubsub.WireModuleLocal // in-process, no dependencies
cpubsub.WireModuleRedis // requires you to provide a *redis.Client
```
### Publishing Events
You should define topics as package constants so that subscribers may import them:
```go theme={null}
const TopicRocketLaunched = "rockets.launched"
err := s.pubsub.Publish(ctx, TopicRocketLaunched, LaunchedEvent{
RocketID: rocket.ID,
})
```
If you are publishing as part of a database transaction, you may wish to publish from an `OnCommit` callback so that the event is only sent once the transaction commits. To learn more, see [Transactions](/database/transactions).
### Subscribing to Topics
It is most common to subscribe in a constructor so that the subscription is in place when the app starts:
```go theme={null}
func NewTelemetry(p NewTelemetryParams) (*Telemetry, error) {
t := &Telemetry{logger: p.Logger}
err := p.PubSub.Subscribe(context.Background(), rockets.TopicRocketLaunched, t.handleRocketLaunched)
if err != nil {
return nil, cerrors.New(err, "failed to subscribe to rocket launches", nil)
}
return t, nil
}
func (t *Telemetry) handleRocketLaunched(ctx context.Context, payload any) error {
// ...
return nil
}
```
Handlers run on managed background goroutines, and subscriptions remain active until the app shuts down.
### Payloads Across Backends
The local backend hands your handler the exact Go value you published. The Redis backend, however, serializes payloads as JSON, so handlers receive the decoded form (a `map[string]any` for structs). If a handler needs the typed struct, you may convert it by marshaling and unmarshaling again:
```go theme={null}
func (t *Telemetry) handleRocketLaunched(ctx context.Context, payload any) error {
var event rockets.LaunchedEvent
raw, err := json.Marshal(payload)
if err != nil {
return cerrors.New(err, "failed to marshal payload", nil)
}
if err := json.Unmarshal(raw, &event); err != nil {
return cerrors.New(err, "failed to unmarshal launched event", nil)
}
// ...
return nil
}
```
`cpubsub` is fire-and-forget: handler errors are logged but not retried, and there are no delivery guarantees or acknowledgments. In addition, the Redis backend broadcasts to every subscribed process. For work that must not be lost, or must run exactly once, consider persisting a job row and letting a worker claim it instead. See [Background Work](/digging-deeper/background-work).
# Sending Email
Source: https://docs.gocopper.dev/digging-deeper/sending-email
The `cmailer` package (from [gocopper/pkg](https://github.com/gocopper/pkg)) provides a simple `Mailer` interface with two backends: AWS SES for production and a log mailer for development. To get started, install the package:
```
go get github.com/gocopper/pkg/cmailer
```
### Setup
`cmailer` does not ship a wire module, so you should provide the constructor for the backend you would like to use directly in your app's wire set:
```go pkg/app/wire.go theme={null}
var WireModule = wire.NewSet(
// ...
cmailer.NewLogMailer, // dev: logs emails instead of sending them
)
```
In production, you may use `cmailer.NewAWSMailer` instead and configure your SES credentials:
```toml config/prod.toml theme={null}
[aws]
region = "us-east-1"
access_key_id = "{{ .EnvVars.AWS_ACCESS_KEY_ID }}"
secret_access_key = "{{ .EnvVars.AWS_SECRET_ACCESS_KEY }}"
```
### Sending
To send an email, inject `cmailer.Mailer` wherever you need it and call its `Send` method:
```go theme={null}
err := s.mailer.Send(ctx, cmailer.SendParams{
From: "mission-control@rocketlog.dev",
To: []string{crew.Email},
Subject: "Launch confirmed",
HTMLBody: cvars.Ptr("
Your rocket launches at T-minus 10.
"),
})
```
`HTMLBody` and `PlainBody` are both optional pointers, and you may set either or both. The `cvars.Ptr` helper (from `github.com/gocopper/pkg/cvars`) saves you a temporary variable.
The log mailer writes the full email to your app's logs, so you can develop email flows without an SES account or a real inbox.
# Testing
Source: https://docs.gocopper.dev/digging-deeper/testing
Copper applications are plain Go, so testing is plain `go test`. Copper helps in two ways: every package's dependencies are explicit, so test doubles drop in naturally, and most Copper packages ship a `test` sibling with ready-made test helpers.
### Testing Handlers
Since handlers are exported methods on a router you construct yourself, you may unit-test them directly with `httptest`, without starting a server:
```go pkg/rockets/router_test.go theme={null}
func TestHandleLaunchRocket(t *testing.T) {
t.Parallel()
router := rockets.NewRouter(rockets.NewRouterParams{
Rockets: &rocketstest.MockSvc{},
RW: chttptest.NewJSONReaderWriter(t),
Logger: clogger.NewNoop(),
})
req := httptest.NewRequest(http.MethodPost, "/api/rockets/falcon/launch", nil)
req = mux.SetURLVars(req, map[string]string{"id": "falcon"})
resp := httptest.NewRecorder()
router.HandleLaunchRocket(resp, req)
require.Equal(t, http.StatusOK, resp.Code)
}
```
`mux.SetURLVars` (from `github.com/gorilla/mux`, Copper's router) injects path parameters when you call a handler directly, since no routing happened. You may skip it for routes without `{params}`.
If you would like to exercise routing, middleware, and handlers together, build a real handler with `chttp.NewHandler` and serve it with `httptest.NewServer`.
### Test Helpers
Most helpers live in a `test` sibling package; a few (like `clogger`'s and `cmetrics`'s) ship in the package itself:
| Helper | Use |
| ------------------------------------------------------------- | -------------------------------------------------------------- |
| `clogger.NewNoop()` | A logger that discards everything; the workhorse of test setup |
| `clogger.NewRecorder(&logs)` | Records logs into a slice so you can assert on them |
| `chttptest.NewJSONReaderWriter(t)` / `NewHTMLReaderWriter(t)` | Ready-made reader/writers for router tests |
| `chttptest.NewRouter(routes)` / `PingRoutes(t, routes)` | Wrap ad-hoc routes; assert every route responds |
| `cconfigtest.SetupDirWithConfigs(t, files)` | Temp config dir from a map of file contents |
| `clifecycletest.New()` | A lifecycle for tests, no signal handling |
| `cmetrics.NewNoopMetrics()` | Metrics that go nowhere |
To assert on your application's logs, record them with `clogger.NewRecorder`:
```go theme={null}
var logs []clogger.RecordedLog
svc := rockets.NewSvc(rockets.NewSvcParams{
Logger: clogger.NewRecorder(&logs),
})
// ...
require.Len(t, logs, 1)
require.Equal(t, clogger.LevelError, logs[0].Level)
```
### Mocking Your Own Packages
You may follow the same convention for your own services. Define an interface in the package, and place a hand-written mock in a `test` sibling so that other packages can use it:
```go pkg/rockets/rocketstest/svc.go theme={null}
package rocketstest
var _ rockets.Svc = (*MockSvc)(nil)
type MockSvc struct {
LaunchErr error
LaunchCalls []string
}
func (m *MockSvc) Launch(ctx context.Context, id string) (*rockets.Launch, error) {
m.LaunchCalls = append(m.LaunchCalls, id)
if m.LaunchErr != nil {
return nil, m.LaunchErr
}
return &rockets.Launch{RocketID: id}, nil
}
```
The `var _ rockets.Svc = (*MockSvc)(nil)` line asks the compiler to verify that the mock stays in sync with the interface, while exported fields like `LaunchErr` allow each test to force the failure it needs.
# Inertia
Source: https://docs.gocopper.dev/frontend/inertia
[Inertia](https://inertiajs.com) lets you build a modern React frontend with classic server-side routing. Your Go routers decide which page to show and what data it receives, while React renders it. Since the server drives navigation, there is no client-side router to maintain and no REST layer between your pages and your services. This is the default stack for new Copper projects.
The `inertia` package (from [gocopper/pkg](https://github.com/gocopper/pkg)) provides the server-side adapter, wired in automatically by `copper create`.
### Rendering a Page
Inject `*inertia.Renderer` into your router and call `Render` with a component name and props:
```go pkg/rockets/router.go theme={null}
func (ro *Router) HandleRocketsPage(w http.ResponseWriter, r *http.Request) {
rockets, err := ro.rockets.List(r.Context())
if err != nil {
ro.logger.Error("Failed to list rockets", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
ro.inertia.Render(w, r, inertia.RenderParams{
Component: "rockets",
Props: map[string]any{
"rockets": rockets,
},
})
}
```
Components live in `web/src/pages/` and receive props directly:
```tsx web/src/pages/rockets.tsx theme={null}
export default function Rockets({ rockets }) {
return (
{rockets.map((rocket) => (
- {rocket.name}
))}
)
}
```
On a full page load, Copper renders the HTML shell with the page data embedded. On navigation, Inertia requests only the JSON; the renderer will detect which is which automatically.
### Forms
You may read and validate a form submission using `ReadForm`, which decodes the JSON body and validates its `valid:` tags ([govalidator](https://github.com/asaskevich/govalidator)). On validation failure, the error is flashed automatically and `ReadForm` returns `false`, so you simply redirect back:
```go theme={null}
func (ro *Router) HandleCreateRocket(w http.ResponseWriter, r *http.Request) {
var form struct {
Name string `json:"name" valid:"required"`
}
if !ro.inertia.ReadForm(w, r, &form) {
ro.inertia.Redirect303(w, r, "/rockets")
return
}
rocket, err := ro.rockets.Create(r.Context(), form.Name)
if err != nil {
ro.inertia.FlashProps(r, map[string]any{
"error": "Could not create the rocket. Please try again.",
})
ro.inertia.Redirect303(w, r, "/rockets")
return
}
ro.inertia.Redirect303(w, r, "/rockets/"+rocket.ID)
}
```
This is the classic post/redirect/get pattern: mutations always end in `Redirect303`, and the next render picks up any flashed props. Validation errors appear on the client under `flash.validationError`.
`ReadForm`'s two failure modes differ: a body that fails validation flashes the error and leaves the response for you to write, but a body that can't be decoded as JSON at all writes an error response itself. Both return `false`.
### Flash Props
`FlashProps` stores props for a single upcoming render, which is perfect for success and error messages across a redirect:
```go theme={null}
ro.inertia.FlashProps(r, map[string]any{
"success": "Rocket launched!",
})
ro.inertia.Redirect303(w, r, "/rockets")
```
The next page render receives them under the `flash` prop, after which they are gone:
```tsx theme={null}
export default function Rockets({ rockets, flash }) {
return (
<>
{flash?.success && {flash.success}}
{/* ... */}
>
)
}
```
### Shared Props
`ShareProps` adds props to the current request's render from anywhere. It is typically called from a middleware so that every page in a section receives common data:
```go theme={null}
func (mw *PropsMiddleware) Handle(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
mw.inertia.ShareProps(r.Context(), map[string]any{
"missionName": mw.config.MissionName,
})
next.ServeHTTP(w, r)
})
}
```
You may attach the middleware per-route via `Middlewares`, or globally in `pkg/app/handler.go`.
`ShareProps` identifies the request via its request ID, so `chttp.SetRequestIDInCtxMiddleware()` must be in your global middlewares. Scaffolded projects include it by default. Successive calls for the same request replace previously shared props rather than merging them, so you should share everything in a single call.
### Partial Reloads
When the client reloads only specific props (`router.reload({ only: ["rockets"] })`), you may skip computing everything else with `ShouldLoadProp`:
```go theme={null}
props := map[string]any{}
if inertia.ShouldLoadProp(r, "telemetry") {
props["telemetry"] = ro.rockets.LoadTelemetry(ctx)
}
```
`ShouldLoadProp` returns `true` on full renders and only for the requested props on partial reloads.
### Deferred Props
Deferred props let a page render immediately while slow data loads in a follow-up request. Declare them in `DeferredProps`, and guard the expensive work with `ShouldLoadDeferredProp`:
```go theme={null}
func (ro *Router) HandleMissionControl(w http.ResponseWriter, r *http.Request) {
props := map[string]any{
"mission": ro.rockets.Mission(r.Context()),
}
if inertia.ShouldLoadDeferredProp(r, "telemetry") {
props["telemetry"] = ro.rockets.LoadTelemetry(r.Context())
}
ro.inertia.Render(w, r, inertia.RenderParams{
Component: "mission-control",
Props: props,
DeferredProps: map[string][]string{
"default": {"telemetry"},
},
})
}
```
Unlike `ShouldLoadProp`, `ShouldLoadDeferredProp` returns `false` on the initial render: the prop is stripped from the first response and fetched immediately afterwards. On the client, wrap the slow section in Inertia's ``:
```tsx theme={null}
}>
```
### Customizing the Renderer
The renderer is immutable; each `With*` method returns a copy, so you may configure it once in your constructor:
```go theme={null}
func NewRouter(p NewRouterParams) *Router {
return &Router{
inertia: p.Inertia.WithBasePath("/admin"),
}
}
```
* `WithBasePath(path)` mounts a section under a URL prefix. Redirects and page URLs have the prefix stripped so that client-side paths stay clean; it pairs with [`chttp.base_path`](/http/routing#serving-under-a-base-path).
* `WithLayoutTemplate(name)` renders pages inside a different layout than `main.html`.
* `WithComponent(name)` sets a default component for all renders.
`RenderParams` accepts per-render `LayoutTemplate` and `BasePath` overrides too.
### Server-Side Rendering
SSR is opt-in. Run the Inertia SSR server (see the [Inertia SSR guide](https://inertiajs.com/server-side-rendering)) and enable it in config:
```toml theme={null}
[inertia]
ssr = true
ssr_server = "http://localhost:13714"
```
Copper sends each page to the SSR server and injects the rendered HTML. If the SSR server is down or errors, Copper logs a warning and falls back to client-side rendering, so SSR will never take your pages down.
### Configuration
| Key | Default | Description |
| ------------ | ------------------------ | ------------------------------------------ |
| `ssr` | `false` | Render pages through an Inertia SSR server |
| `ssr_server` | `http://localhost:13714` | Address of the SSR server |
# Overview
Source: https://docs.gocopper.dev/frontend/overview
Copper treats the frontend as part of your application rather than an afterthought. React and Inertia are the R and I of the [GRIP stack](/): the dev server, asset pipeline, and production builds are all handled by the CLI, and your Go routers render React pages directly.
### Choosing a Stack
`copper create` supports two frontend stacks via the `-frontend` flag:
* **`inertia:react`** (default) is a React app powered by [Vite](/frontend/vite) and [Inertia](/frontend/inertia). Your Go routers render React pages directly, giving you server-side routing and client-side interactivity without a separate API layer or client router to maintain.
* **`none`** creates no `web/` directory, which is ideal for JSON APIs or for classic server-rendered [HTML views](/http/html-views) using Go templates.
```
copper create -frontend=inertia:react github.com/gocopper/rockets
```
### Development
A single command runs everything:
```
copper run -watch
```
This starts your app on `http://localhost:5901` and the Vite dev server on port `3000`. Copper injects the Vite client into your pages, so hot module replacement works out of the box: edit a React component and the browser updates without a reload. Go changes will trigger a rebuild and restart automatically.
The scaffolded `config/dev.toml` enables the dev-mode behavior:
```toml config/dev.toml theme={null}
[vitejs]
dev_mode = true
[chttp]
use_local_html = true
render_html_error = true
```
### Production Builds
To build for production, run the frontend build followed by `copper build`:
```
cd web && npm run build && cd ..
copper build
```
`npm run build` (or `bun run build`) runs the Vite production build, outputting hashed assets into `web/build/static`. `copper build` then copies `web/public` in alongside them and generates `web/build/embed.go` so that all assets compile into your binary. Deploying a Copper app, frontend included, is a matter of copying a single file.
### The web Directory
```
web/
├── wire.go # embeds src/ and registers render funcs
├── public/ # static files served as-is under /static
├── src/
│ ├── layouts/main.html # HTML shell rendered by the server
│ ├── pages/ # your React page components
│ ├── main.tsx # frontend entrypoint
│ └── styles.css
├── vite.config.ts
└── package.json
```
`web/wire.go` is the bridge between the frontend and Go: it embeds the templates and registers the [Vite render func](/frontend/vite) that emits the right script tags:
```go web/wire.go theme={null}
//go:embed src
var HTMLDir embed.FS
func HTMLRenderFuncs(vite *vitejs.Assets) []chttp.HTMLRenderFunc {
return []chttp.HTMLRenderFunc{
vite.HTMLRenderFunc(),
}
}
```
Copper uses [bun](https://bun.sh) when it is installed and falls back to npm, both for installing dependencies at scaffold time and for running the dev server.
# Vite
Source: https://docs.gocopper.dev/frontend/vite
The `vitejs` package (from [gocopper/pkg](https://github.com/gocopper/pkg)) connects your Go templates to [Vite](https://vitejs.dev): the dev server during development, and hashed production assets in a release build. Your templates do not change between the two.
### viteAssets
The layout calls a single template function in its ``:
```html web/src/layouts/main.html theme={null}
{{ viteAssets }}
{{ template "content" . }}
```
`viteAssets` emits whatever the current mode needs, so you never write script tags by hand.
### Development Mode
With `dev_mode` enabled, `viteAssets` points the page at the Vite dev server:
```toml config/dev.toml theme={null}
[vitejs]
dev_mode = true
```
It injects the Vite client and your entrypoint as module scripts, along with the React fast-refresh preamble, so HMR works without any frontend configuration. Copper discovers your entrypoint by convention: `src/main.js`, `.ts`, `.jsx`, or `.tsx`.
### Production Mode
When `dev_mode` is off (the default), `viteAssets` reads the Vite build manifest from the assets embedded in your binary and emits the hashed `