


Build a Full-Stack TypeScript App on API Schemas Shared by Server and Client
Use when building or changing a full-stack TypeScript app. The AI defines each API request and response once for routes and client hooks, checks sign-in before reading input, keeps dependencies, caching, and component styles consistent, and writes fast tests.
To use this AI rule
## Keep Package Dependencies One-Way and Decide Where Each Kind of State Lives
Organize the codebase so ownership, dependency direction, and state placement remain predictable as the system grows.
### Structure by responsibility
- Give every component, hook, store, service, and package one clear purpose, and prefer composition of small units over adding branches and modes to an already complex unit.
- Keep application code above reusable packages in the dependency graph; lower-level packages must not import application modules.
- Keep isolated services free of unrelated UI frameworks, registries, provider SDKs, and execution engines.
- Share authentication through one compatible session and data model rather than reimplementing it per application.
### Preserve type and state ownership
- Define explicit interfaces for component properties, state, function inputs, and return values.
- Do not use `any`; use `unknown` with type guards when the type is genuinely uncertain.
- Keep server state in the server-state library, global client state in a store, and local presentation state in the component.
- Split a complex store into implementation and type modules; persist only state that must survive reloads and select persisted fields explicitly.
## Use the Repo's Shared Helpers and Tools Instead of Local Copies
Use one canonical toolchain and shared implementation patterns instead of creating local substitutes.
### Apply consistent code conventions
- Use the repository’s chosen package manager, formatter, linter, type checker, and audit commands.
- Use structured logging rather than console output; request-scoped route wrappers should attach request identifiers automatically.
- Follow consistent naming and import ordering; use type-only imports, constant assertions, explicit reference element types, and TSDoc rather than decorative or unstructured comments.
- Prefer absolute imports. Create a barrel only when a folder has at least 3 exports, and do not re-export from non-barrel files.
### Reuse proven helpers
- Use shared helpers for ID generation, delays, error normalization, message extraction, deep cloning, object filtering, truncation, and retry backoff with jitter; do not recreate them with local packages or one-off logic.
- Inline a helper used by 1 consumer; move it to a utility module when at least 2 files need it.
- Extract a component when it exceeds roughly 50 lines, serves at least 2 callers, or owns state or logic; keep a purely presentational single-use fragment under 10 lines inline.
## Define Each API Request and Response Once, and Explain Every Raw Fetch
Define every HTTP request and response once so routes and clients cannot drift into incompatible wire formats.
### Make contracts the boundary source of truth
- Keep one contract module per resource family, with separate schemas for path parameters, query values, body, headers, and the response combined in one route contract.
- Export named schemas and named input and output type aliases for clients, and reuse shared identifier primitives instead of deriving ad hoc wire types or redefining string-based identifiers.
- Allow domain validators to use the schema library directly, but keep route boundary schemas out of route handlers and clients.
### Control exceptions explicitly
- Require a reasoned annotation immediately above any exceptional raw fetch, double cast, raw JSON read, or deliberately untyped response.
- Permit raw fetch only for cases such as streaming, binary transfer, multipart upload, signed URLs, OAuth redirects, or external origins.
- Permit raw JSON reads only for envelopes or tolerant parsing that cannot use the standard parser, and untyped responses only for genuinely opaque data.
- Keep legitimate whole-file exceptions in a reviewed allowlist rather than scattering line-level bypasses.
- Make a strict boundary audit fail on missing reasons, route-local schemas, raw reads, unapproved fetches, and unsafe double casts.
## Add an API Route and Its Client Hook, Starting from the Schema
Add a boundary feature in a fixed sequence so authentication, validation, response typing, and caching agree end to end.
### Follow the implementation sequence
1. Author the contract and its named input and output types.
2. Wrap the route in the canonical request-context and error-handling wrapper, composing shared authentication, rate-limit, and access-control middleware instead of reimplementing them per route.
3. Authenticate and authorize before parsing untrusted input, then parse all request slices through the contract.
4. Return exactly the JSON shape declared by the response schema.
5. Add a typed client hook that sends and validates data through the same contract.
6. Use the hook in the component and surface its typed error message.
### Review schemas like migrations
- Distinguish omission from `null`; combining optional and nullable creates a third state and should be intentional.
- Compare every response call site with the declared response schema.
- Set explicit bounds on strings, arrays, limits, and sizes, with descriptive field-specific errors.
- Treat unknown schemas as exceptional and document why the payload is opaque; prefer discriminated unions when the wire format contains a discriminator.
- Use human review for semantic quality; structural audits cannot detect every weak bound or inaccurate response shape.
## Keep the Query Cache Correct Through Cancels and Optimistic Updates
Make cancellation, caching, invalidation, and optimistic updates explicit rather than hiding server state in component state.
### Build stable hooks and query keys
- Use a reference when a stable callback must read the latest input, and update that reference when the input changes.
- Forward the cancellation signal from every query function and give every query an explicit freshness interval.
- Build hierarchical keys from a root through list and detail prefixes for targeted invalidation, and keep previous data only for variable-key queries, not static queries.
### Reconcile mutations safely
- Before an optimistic update, cancel affected queries and save the previous cache value so it can be restored on error.
- Reconcile and invalidate on settlement, not only on success, because both success and failure require cache consistency.
- Prefer targeted invalidation over invalidating the entire resource tree.
- Do not add stable mutation methods or objects to callback dependencies unnecessarily.
- Route same-origin JSON through the typed request helper; raw fetch exceptions must follow the documented boundary policy.
## Keep Each Shared Component's Look Inside It, Adjusted Through Props
Treat shared component appearance as a single source of truth owned by the component rather than each consumer.
### Keep presentation local and composable
- Do not modify global styles for a local feature; scope styling to components.
- Use the project’s conditional-class helper and dimension shorthand instead of duplicating class-building logic.
- Use variant tooling only when a component has at least 2 genuine variants; use a simple conditional class for one state toggle.
- Import shared components through the supported public entry point rather than internal subpaths.
- Verify that referenced CSS variables exist because an undefined variable can silently fall back to an unintended color.
### Drive intent through properties
- Expose properties for errors, adornments, labels, hints, required state, and behavioral modes; reserve consumer class overrides for layout and sizing rather than canonical borders, fills, radii, heights, colors, or focus treatment.
- Use discriminated-union properties for modes instead of maintaining near-duplicate components.
- Use the canonical labeled-field wrapper inside modal bodies so field gutters align with headers and footers.
- Remove legacy variants after migration instead of leaving parallel design-system paths.
## Write Fast Vitest Tests with Shared Mocks and Fake Timers
Use the repository’s standard test runner, global fixtures, and deterministic timing patterns.
### Isolate behavior
- Name each test beside its source using the repository’s standard test suffix.
- Reuse global mocks and shared factories, replacing one only when a test needs different behavior; clear mock state before each test and define per-test return values explicitly.
- Mock heavy registries and provider dependencies when the test does not exercise them.
- Use a server runtime environment unless browser APIs are required.
- Avoid real timers; use fake timers or the smallest deterministic delay the behavior requires.
### Avoid expensive module churn
- In Vitest, prefer hoisted mocks and static imports.
- Do not combine module resets, late mocks, and dynamic imports, and do not import real implementations through `importActual` when explicit mocks suffice.
- Keep authentication and logging mocks explicit rather than calling helpers that hide late mocking.
- Run the strict API-boundary audit with the relevant tests before review.
## Add Integrations Without Converting Variables Before They Resolve
Build integrations in dependency order and preserve unresolved references until execution.
### Follow the integration sequence
1. Define and register tools with stable `snake_case` identifiers.
2. Add the consuming block and register it in the canonical order.
3. Add the icon and required metadata.
4. Add a trigger only when the integration needs one.
5. Verify the service behavior against authoritative API documentation.
### Respect lifecycle timing
- Distinguish serialization-time configuration from execution-time parameter resolution.
- Never coerce types while serializing unresolved values; doing so can destroy dynamic references.
- Perform numeric and other coercions only after variables resolve during execution.
- Keep tool, block, trigger, authentication, and file-input metadata synchronized.
- Run linting, type checks, tests, and strict boundary audits, then review the final diff for contract, cache, styling, and registry consistency.
