Step 9 — The Shared Contract: shared/types.ts

Step 7's Tag, CreateTag, and UpdateTag Rust structs need a TypeScript equivalent so the frontend can work with the same shapes. That equivalent lives in shared/types.ts (imported via the shared alias from Step 3) — a single file the whole frontend imports its API types from.

The three types, side by side with Rust

export type Tag = {
  id: string,
  tag_name: string,
  content: string,
  color: string | null,
  team_id: string | null,
  created_at: string,
  updated_at: string,
};

export type CreateTag = {
  tag_name: string,
  content: string,
  color?: string | null,
  team_id?: string | null,
};

export type UpdateTag = {
  tag_name: string | null,
  content: string | null,
  color?: string | null,
  team_id?: string | null,
};

Compare this to Step 7's Rust:

Why this file is hand-maintained, not generated

You might expect a tool to generate types.ts automatically from the Rust structs — and iKanban used to run one (ts-rs). That generator is retired; shared/types.ts is now edited by hand whenever a Rust struct changes shape. In practice this means: if you change a field in crates/remote/src/db/tags.rs, you must also update shared/types.ts yourself — nothing enforces the two stay in sync automatically. This is exactly the kind of contract Step 7's preflight-style thinking exists to protect: a silently-drifted shared type doesn't fail at compile time, it fails as a runtime shape mismatch the first time real data flows through it.

Where else this type is used

Tag doesn't just describe one row — CLAUDE.md's "borrow before you build" principle shows up here too. Rather than each frontend file inventing its own tag shape, everything downstream imports the same Tag from shared/types:

import type { CreateTag, Tag, UpdateTag } from 'shared/types';

You'll see this exact import in Step 10's API client, Step 11's React hook, and Step 12's dialog component — one type, one source of truth, four consumers.

Checkpoint

Search the frontend for TaskTagWithDetails in shared/types.ts — it's a fourth, denormalized shape (a tag joined with its assignment to a specific task) that exists purely because a task's tag list needs more context than a bare Tag provides. Notice it's a distinct type rather than Tag with extra optional fields bolted on — the same "don't overload one struct for every use case" instinct from Step 7's CreateTag/UpdateTag split.

Continue to Step 10 — The API Client: tagsApi in lib/api.ts, where these types get used to call the routes from Step 8.