Skip to main content
In this tutorial, we’ll build rocketlog, a small application on the GRIP stack that tracks a fleet of rockets. Along the way, you’ll touch every layer of a Copper application: migrations, queries, routing, and a React page rendered from your Go handlers. You’ll need the Copper CLI installed and Postgres running. If you don’t have Postgres handy, Docker can start one:

Creating the Project

Let’s create the project and start it in watch mode:
Once it starts, your application will be accessible in your browser at http://localhost:5901. Leave copper run -watch running while you work; it rebuilds and restarts the application whenever you save a Go file under pkg/, and the Vite dev server hot-reloads your React changes.

Defining the Schema

New projects come with an empty first migration. Let’s add a rockets table to it:
migrations/0001_initial.sql
copper run applies pending migrations each time it starts, so restart it (or run copper migrate) to create the table. Later migrations get their own sequentially numbered files; see Migrations for more.

Scaffolding the Feature Package

Each feature in a Copper application lives in its own package under pkg/. Let’s scaffold one for rockets, along with a queries struct for database access:
This creates pkg/rockets/ with models.go, queries.go, and wire.go, and registers the package with the application. Next, define the model:
pkg/rockets/models.go
The db tags map database columns to fields, while the json tags shape the props your React page receives.

Writing Queries

Now let’s add two methods to the scaffolded Queries struct:
pkg/rockets/queries.go
You may write placeholders as ? regardless of your database; Copper rebinds them to the right style ($1 for Postgres) automatically. The qb.Columns helper (from github.com/gocopper/copper/csql/qb) renders the column list from the model’s db: tags, so the query never drifts from the struct; see Query Builder for more.

Adding Routes

Next, scaffold a router for the package:
This creates pkg/rockets/router.go and registers it in pkg/app/handler.go. The scaffolded router only receives a logger, but our handlers also need the queries and the Inertia renderer. Add them to NewRouterParams, and Copper’s dependency injection provides them:
pkg/rockets/router.go
There is no registration or container configuration to write. Since dependencies are resolved at compile time, Copper sees the new fields and provides them automatically. To learn more, see Dependency Injection. Now let’s declare the routes and handlers, one to render the page and one to handle the form:
pkg/rockets/router.go
ReadForm decodes the request and enforces the valid tags. If validation fails, it flashes the validation error and returns false, and the redirect sends the user back to the form. This POST-then-redirect shape is the standard Inertia flow; see Inertia for the details.

Building the Page

The Component: "rockets" above resolves to web/src/pages/rockets.tsx. Let’s create it:
web/src/pages/rockets.tsx
The props come straight from your handler, so there is no fetch call or API client to write. Inertia’s useForm posts the form and re-renders the page with fresh props after the redirect.

Trying It Out

Open http://localhost:5901/rockets, type a name, and add a rocket. The list updates instantly. If you submit an empty name, valid:"required" rejects it and the redirect brings you back. You’ve now built a full-stack feature, from the database to React, without writing an API.

Next Steps