# Step 7 — The Database Layer: TagRepository in Rust

Part 2 traces one real, already-shipped iKanban feature — **Tags** — through every layer of the stack: database → route → shared type contract → API client → React hook → component. We start at the bottom: `crates/remote/src/db/tags.rs`.

## Why start at the database?

Frontend engineers usually meet a feature at the component. Working backend-to-frontend instead makes each layer's *reason to exist* obvious: the component only needs what the hook gives it, the hook only needs what the API client returns, and so on — each layer exists because the one below it isn't shaped right for the one above it. You don't need to read Rust fluently to follow this; the point is the **shape** of the data at each hop.

## The `Tag` struct

```rust
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Tag {
    pub id: Uuid,
    pub tag_name: String,
    pub content: String,
    pub color: Option<String>,
    pub team_id: Option<Uuid>,
    pub created_at: DateTime<Utc>,
    pub updated_at: DateTime<Utc>,
}
```

`#[derive(Serialize, Deserialize)]` (from the `serde` crate) means this struct can be turned into JSON and back automatically. Every field name here — `tag_name`, `content`, `color` — will reappear **verbatim** as a JSON key by the time it reaches the browser. Keep this struct in mind; you'll see its exact shape mirrored in a hand-written TypeScript type in Step 9.

## `TagRepository` — one method per operation

```rust
pub struct TagRepository;

impl TagRepository {
    pub async fn find_by_team(pool: &PgPool, team_id: Uuid) -> Result<Vec<Tag>, TagError> {
        let rows = sqlx::query!(
            r#"
            SELECT id AS "id!: Uuid", tag_name AS "tag_name!", content AS "content!",
                   color, team_id AS "team_id: Uuid",
                   created_at AS "created_at!: DateTime<Utc>", updated_at AS "updated_at!: DateTime<Utc>"
            FROM tags
            WHERE team_id = $1
            ORDER BY tag_name ASC
            "#,
            team_id
        )
        .fetch_all(pool)
        .await?;

        Ok(rows.into_iter().map(|r| Tag { id: r.id, tag_name: r.tag_name, /* ... */ }).collect())
    }

    pub async fn create(pool: &PgPool, payload: &CreateTag) -> Result<Tag, TagError> { /* ... */ }
    pub async fn update(pool: &PgPool, id: Uuid, payload: &UpdateTag) -> Result<Tag, TagError> { /* ... */ }
    pub async fn delete(pool: &PgPool, id: Uuid) -> Result<bool, TagError> { /* ... */ }
}
```

`sqlx::query!` is a compile-time-checked macro — the SQL string is validated against the *real* database schema when the project builds (via the SQLx offline cache, `crates/remote/.sqlx/`). A typo'd column name is a build failure, not a runtime surprise — the same "compiler catches it first" guarantee the `rust-as-backend` tutorial covers in depth for the Rust language itself.

## `CreateTag` and `UpdateTag` — narrower structs for writes

```rust
#[derive(Debug, Clone, Deserialize)]
pub struct CreateTag {
    pub tag_name: String,
    pub content: Option<String>,
    pub color: Option<String>,
    pub team_id: Option<Uuid>,
}

#[derive(Debug, Clone, Deserialize)]
pub struct UpdateTag {
    pub tag_name: Option<String>,
    pub content: Option<String>,
    pub color: Option<String>,
}
```

Notice these are **separate structs from `Tag`**, not `Tag` with some fields left blank. `CreateTag` has no `id` (the database assigns it) and no timestamps (the database sets those too). `UpdateTag` makes every field `Option` — `None` means "leave this field unchanged," handled explicitly in `TagRepository::update`:

```rust
let tag_name = payload.tag_name.as_ref().unwrap_or(&existing.tag_name);
```

This three-struct pattern (`Tag` / `CreateTag` / `UpdateTag`) is the standard shape for any resource in this codebase — you'll see it mirrored again in the frontend's type file in Step 9.

## Checkpoint

Open `vibe-backend/crates/remote/src/db/tags.rs` and find `sanitize_color` — notice it falls back to a `DEFAULT_TAG_COLOR` constant rather than rejecting an invalid hex color outright. That's a deliberate "don't error the whole request over a cosmetic field" choice.

Continue to **Step 8 — The Route Layer: Axum Handlers for `/api/tags`**, where this repository gets exposed over HTTP.
