> ## Documentation Index
> Fetch the complete documentation index at: https://docs.gocopper.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Tutorial: Your First App

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](/getting-started/installation) and Postgres running. If you don't have Postgres handy, Docker can start one:

```bash theme={null}
docker run -d --name rocketlog-db -p 5432:5432 -e POSTGRES_PASSWORD=dev postgres:16
```

### Creating the Project

Let's create the project and start it in watch mode:

```
copper create github.com/gocopper/rocketlog
cd rocketlog
copper run -watch
```

Once it starts, your application will be accessible in your browser at [http://localhost:5901](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:

```sql migrations/0001_initial.sql theme={null}
-- +migrate Up
CREATE TABLE rockets (
    id         SERIAL PRIMARY KEY,
    name       TEXT NOT NULL,
    status     TEXT NOT NULL DEFAULT 'ready',
    created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

-- +migrate Down
DROP TABLE rockets;
```

`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](/database/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:

```
copper scaffold:pkg rockets
copper scaffold:queries rockets
```

This creates `pkg/rockets/` with `models.go`, `queries.go`, and `wire.go`, and registers the package with the application. Next, define the model:

```go pkg/rockets/models.go theme={null}
package rockets

import "time"

type Rocket struct {
	ID        int64     `db:"id" json:"id"`
	Name      string    `db:"name" json:"name"`
	Status    string    `db:"status" json:"status"`
	CreatedAt time.Time `db:"created_at" json:"createdAt"`
}
```

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:

```go pkg/rockets/queries.go theme={null}
func (q *Queries) ListRockets(ctx context.Context) ([]Rocket, error) {
	query := fmt.Sprintf("SELECT %s FROM rockets ORDER BY created_at DESC", qb.Columns(Rocket{}))

	var rockets []Rocket

	err := q.querier.Select(ctx, &rockets, query)
	if err != nil {
		return nil, err
	}

	return rockets, nil
}

func (q *Queries) InsertRocket(ctx context.Context, name string) error {
	const query = "INSERT INTO rockets (name) VALUES (?)"

	_, err := q.querier.Exec(ctx, query, name)

	return err
}
```

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](/database/query-builder) for more.

### Adding Routes

Next, scaffold a router for the package:

```
copper scaffold:router rockets
```

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:

```go pkg/rockets/router.go theme={null}
type NewRouterParams struct {
	Queries *Queries
	Inertia *inertia.Renderer
	Logger  clogger.Logger
}

func NewRouter(p NewRouterParams) *Router {
	return &Router{
		queries: p.Queries,
		inertia: p.Inertia,
		logger:  p.Logger,
	}
}

type Router struct {
	queries *Queries
	inertia *inertia.Renderer
	logger  clogger.Logger
}
```

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](/architecture/dependency-injection).

Now let's declare the routes and handlers, one to render the page and one to handle the form:

```go pkg/rockets/router.go theme={null}
func (ro *Router) Routes() []chttp.Route {
	return []chttp.Route{
		{
			Path:    "/rockets",
			Methods: []string{http.MethodGet},
			Handler: ro.HandleRocketsPage,
		},
		{
			Path:    "/rockets",
			Methods: []string{http.MethodPost},
			Handler: ro.HandleCreateRocket,
		},
	}
}

func (ro *Router) HandleRocketsPage(w http.ResponseWriter, r *http.Request) {
	rockets, err := ro.queries.ListRockets(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,
		},
	})
}

func (ro *Router) HandleCreateRocket(w http.ResponseWriter, r *http.Request) {
	var form struct {
		Name string `json:"name" valid:"required"`
	}

	if ok := ro.inertia.ReadForm(w, r, &form); !ok {
		ro.inertia.Redirect303(w, r, "/rockets")
		return
	}

	err := ro.queries.InsertRocket(r.Context(), form.Name)
	if err != nil {
		ro.logger.Error("Failed to create rocket", err)
		w.WriteHeader(http.StatusInternalServerError)
		return
	}

	ro.inertia.Redirect303(w, r, "/rockets")
}
```

`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](/frontend/inertia) for the details.

### Building the Page

The `Component: "rockets"` above resolves to `web/src/pages/rockets.tsx`. Let's create it:

```tsx web/src/pages/rockets.tsx theme={null}
import { useForm } from "@inertiajs/react";

type Rocket = { id: number; name: string; status: string };

export default function Rockets({ rockets }: { rockets: Rocket[] }) {
  const form = useForm({ name: "" });

  return (
    <div>
      <h1>Fleet</h1>

      <ul>
        {rockets.map((rocket) => (
          <li key={rocket.id}>
            {rocket.name} — {rocket.status}
          </li>
        ))}
      </ul>

      <form
        onSubmit={(e) => {
          e.preventDefault();
          form.post("/rockets", { onSuccess: () => form.reset() });
        }}
      >
        <input
          value={form.data.name}
          onChange={(e) => form.setData("name", e.target.value)}
          placeholder="Rocket name"
        />
        <button type="submit">Add rocket</button>
      </form>
    </div>
  );
}
```

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](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

* Go deeper on [Inertia](/frontend/inertia): flash messages, shared props, partial reloads
* Wrap multi-step writes in [Transactions](/database/transactions)
* Expose the same data as a [JSON API](/http/json)
* Add [Middleware](/http/middleware) for auth or logging
* Understand the [App Lifecycle](/architecture/app-lifecycle) behind `copper.New()`
