skills/practices/forge-frontend-integration/SKILL.md
# Forge Frontend Integration When a project uses an RTG Forge backend module, the frontend must integrate with its API cleanly. This skill defines how to build the TypeScript layer that bridges a forge module's API to React components. The frontend integration has three layers. Build them in order — each one depends on the previous. ``` Layer 1: types.ts ← mirrors models.py (portable, rarely changes) Layer 2: hooks.ts ← typed React Query hooks per endpoint (mostly portable) Layer 3:
npx skillsauth add 33prime/rtg-forge skills/practices/forge-frontend-integrationInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
When a project uses an RTG Forge backend module, the frontend must integrate with its API cleanly. This skill defines how to build the TypeScript layer that bridges a forge module's API to React components.
The frontend integration has three layers. Build them in order — each one depends on the previous.
Layer 1: types.ts ← mirrors models.py (portable, rarely changes)
Layer 2: hooks.ts ← typed React Query hooks per endpoint (mostly portable)
Layer 3: components ← UI that consumes hooks (project-specific, built fresh)
Every forge module has a models.py with Pydantic schemas. The frontend gets a types.ts that mirrors them exactly.
snake_case, use camelCase only if the API serializes that way (check the backend's model_config for alias generators). When in doubt, match the JSON the API actually returns.z.infer or plain interfaces — pick one per project and be consistent. If the project uses Zod, define schemas and infer types. If not, use plain TypeScript interfaces.any. If you don't know the shape, read the backend models.py and type it correctly.// types.ts — mirrors participant_intake/models.py
export interface FormDefinition {
id: string;
title: string;
description: string;
questions: FormQuestion[];
status: FormStatus;
created_at: string;
}
export interface FormQuestion {
id: string;
label: string;
type: QuestionType;
required: boolean;
options?: string[]; // only for 'select' | 'multiselect'
validation?: string; // regex pattern
}
export type QuestionType = 'text' | 'textarea' | 'select' | 'multiselect' | 'number' | 'date' | 'email';
export type FormStatus = 'draft' | 'active' | 'closed';
// --- Request types ---
export interface CreateFormRequest {
title: string;
description: string;
questions: Omit<FormQuestion, 'id'>[];
}
export interface SubmitResponseRequest {
participant_id: string;
answers: Record<string, string | string[] | number>;
}
// --- Response types ---
export interface FormListResponse {
items: FormDefinition[];
total: number;
}
export interface SubmissionResponse {
id: string;
participant_id: string;
form_id: string;
answers: Record<string, string | string[] | number>;
submitted_at: string;
}
export interface ProgressResponse {
participant_id: string;
completed_forms: number;
total_forms: number;
percent_complete: number;
current_step: string | null;
}
Read the module's models.py and translate each model:
| Python (Pydantic) | TypeScript |
|---|---|
| str | string |
| int, float, Decimal | number |
| bool | boolean |
| UUID | string |
| datetime | string (ISO 8601) |
| list[T] | T[] |
| dict[str, T] | Record<string, T> |
| T \| None | T \| null |
| Literal["a", "b"] | 'a' \| 'b' (union type) |
| Enum class | type X = 'value1' \| 'value2' |
Every API endpoint gets a dedicated React Query hook. No component ever calls fetch directly.
useFormDefinitions(), useFormDefinition(id), useSubmitResponse(), useParticipantProgress(id). Never a generic useApi().useQuery for reads, useMutation for writes.{ data, isLoading, error }. Don't pre-process.types.ts. No any, no unknown that gets cast downstream.// hooks.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import type {
FormDefinition,
FormListResponse,
CreateFormRequest,
SubmitResponseRequest,
SubmissionResponse,
ProgressResponse,
} from './types';
const API_BASE = '/api/v1/intake';
// --- Queries ---
export function useFormDefinitions(params?: { limit?: number; offset?: number }) {
return useQuery<FormListResponse>({
queryKey: ['forms', params],
queryFn: async () => {
const search = new URLSearchParams();
if (params?.limit) search.set('limit', String(params.limit));
if (params?.offset) search.set('offset', String(params.offset));
const res = await fetch(`${API_BASE}/forms?${search}`);
if (!res.ok) throw new Error('Failed to load forms');
return res.json();
},
});
}
export function useFormDefinition(formId: string) {
return useQuery<FormDefinition>({
queryKey: ['forms', formId],
queryFn: async () => {
const res = await fetch(`${API_BASE}/forms/${formId}`);
if (!res.ok) throw new Error('Form not found');
return res.json();
},
enabled: !!formId,
});
}
export function useParticipantProgress(participantId: string) {
return useQuery<ProgressResponse>({
queryKey: ['progress', participantId],
queryFn: async () => {
const res = await fetch(`${API_BASE}/progress/${participantId}`);
if (!res.ok) throw new Error('Failed to load progress');
return res.json();
},
enabled: !!participantId,
});
}
// --- Mutations ---
export function useCreateForm() {
const queryClient = useQueryClient();
return useMutation<FormDefinition, Error, CreateFormRequest>({
mutationFn: async (body) => {
const res = await fetch(`${API_BASE}/forms`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
if (!res.ok) throw new Error('Failed to create form');
return res.json();
},
onSuccess: () => {
void queryClient.invalidateQueries({ queryKey: ['forms'] });
},
});
}
export function useSubmitResponse() {
const queryClient = useQueryClient();
return useMutation<SubmissionResponse, Error, SubmitResponseRequest & { formId: string }>({
mutationFn: async ({ formId, ...body }) => {
const res = await fetch(`${API_BASE}/forms/${formId}/submit`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
if (!res.ok) throw new Error('Failed to submit response');
return res.json();
},
onSuccess: (_, variables) => {
void queryClient.invalidateQueries({ queryKey: ['progress', variables.participant_id] });
void queryClient.invalidateQueries({ queryKey: ['forms'] });
},
});
}
| Endpoint | HTTP | Hook Name |
|---|---|---|
| GET /forms | query | useFormDefinitions() |
| GET /forms/:id | query | useFormDefinition(id) |
| POST /forms | mutation | useCreateForm() |
| PUT /forms/:id | mutation | useUpdateForm() |
| DELETE /forms/:id | mutation | useDeleteForm() |
| POST /forms/:id/submit | mutation | useSubmitResponse() |
| GET /progress/:id | query | useParticipantProgress(id) |
Prefix with use. Queries are nouns (useFormDefinitions). Mutations are verbs (useCreateForm, useSubmitResponse).
Components consume hooks and render UI. This layer is always project-specific — it uses the project's design system, layout conventions, and interaction patterns.
fetch calls in components.FormQuestion component receives props and renders. A FormPage component calls hooks and orchestrates.features/
intake/
types.ts # Layer 1
hooks.ts # Layer 2
FormPage.tsx # page that orchestrates
FormRenderer.tsx # renders a form definition
QuestionField.tsx # renders a single question
ProgressBar.tsx # shows participant progress
// FormPage.tsx — orchestration component
import { useParams } from 'react-router-dom';
import { useFormDefinition, useSubmitResponse } from './hooks';
import FormRenderer from './FormRenderer';
export default function FormPage() {
const { formId } = useParams<{ formId: string }>();
const { data: form, isLoading, error } = useFormDefinition(formId!);
const submitMutation = useSubmitResponse();
if (isLoading) return <LoadingSkeleton />;
if (error) return <ErrorState message="Could not load form" />;
if (!form) return null;
return (
<FormRenderer
form={form}
onSubmit={(answers) => {
submitMutation.mutate({
formId: form.id,
participant_id: currentParticipantId,
answers,
});
}}
isSubmitting={submitMutation.isPending}
/>
);
}
// FormRenderer.tsx — presentational component
import type { FormDefinition } from './types';
import QuestionField from './QuestionField';
interface FormRendererProps {
form: FormDefinition;
onSubmit: (answers: Record<string, string | string[] | number>) => void;
isSubmitting: boolean;
}
export default function FormRenderer({ form, onSubmit, isSubmitting }: FormRendererProps) {
// renders questions, collects answers, calls onSubmit
// uses PROJECT-SPECIFIC styling, form library, validation
}
Notice: FormRenderer knows nothing about the API. It receives typed data and a callback. This is the cleanest boundary for a component that varies per project.
When a forge module includes a frontend/ directory, here's what's portable and what's reference:
| File | Portable? | Notes |
|---|---|---|
| types.ts | Yes | Mirror of models.py. Copy directly. |
| hooks.ts | Mostly | Works as-is if the project uses React Query. Only change is API_BASE. |
| *.tsx components | No — reference only | Adapt to the project's design system. |
When Claude adapts a module via /use-module, it should:
types.ts as-is (adjusting field casing if the backend uses a camelCase alias)hooks.ts — update API_BASE, adjust to the project's fetching patterns| Question | Answer |
|----------|--------|
| Where do API types go? | types.ts next to the feature — never in a global types/ folder |
| Can components call fetch? | No. Components call hooks. Hooks call the API. |
| How do I type API responses? | Mirror the backend models.py into TypeScript interfaces |
| What fetching library? | React Query (@tanstack/react-query). One hook per endpoint. |
| How do I handle loading states? | Destructure { data, isLoading, error } from every query hook |
| Where does API_BASE live? | One constant at the top of hooks.ts, or in an env-based config |
| Should I use Zod or plain interfaces? | Match whatever the project already uses. Be consistent. |
| What about WebSocket/realtime? | Supabase realtime subscriptions go in hooks too, same pattern |
development
# Parallel Execution > This skill is under development. Workflow patterns for running independent tasks in parallel to improve performance and throughput. ## Topics to Cover - Identifying independent tasks suitable for parallel execution - `asyncio.gather()` with `return_exceptions=True` - `asyncio.TaskGroup` for structured concurrency (Python 3.11+) - Semaphores for bounded concurrency - `Promise.all()` and `Promise.allSettled()` in TypeScript - Handling partial failures (some tasks succeed
development
# Module Extraction > This skill is under development. Workflow for identifying and extracting reusable modules from existing codebases. Extract when a pattern is used in 3+ places and has stabilized. ## Topics to Cover - Identifying extraction candidates (rule of three) - Defining module boundaries and public interface - Dependency analysis: what does the module need? - Interface design: protocols, abstract base classes - Step-by-step extraction process - Testing strategy: tests before, dur
development
# Forge Orchestrate — Intelligent Build Orchestration You are a build planner, not a build executor. Your job is to look at a project, figure out what's left to build, decompose the work into parallel streams, assign the right intelligence level to each stream, estimate cost, and hand the user a set of terminal commands they can run. You plan. They execute. --- ## Stream Decomposition The unit of parallelism is a **stream** — a self-contained bundle of tasks that one Claude session handles e
development
# Code Review > This skill is under development. Workflow for conducting effective code reviews that catch real issues and improve code quality. ## Topics to Cover - Review priorities: correctness > design > performance > style - What to check in every review (checklist) - How to give constructive feedback - Automated checks that should run before human review - Review scope: how big is too big? - Patterns for reviewing database migrations - Patterns for reviewing API changes - When to reque