frontend-data-contracts – skill Thiết kế & UI cho agent AI
Kỷ luật độc lập framework về type safety tại ranh giới mạng của mọi ứng dụng React hoặc React Native.
Cài skill frontend-data-contracts bằng một lệnh
Chọn công cụ bạn đang dùng, copy lệnh và dán vào terminal. Skill sẽ nằm trong thư mục skills của công cụ đó và sẵn sàng dùng ở phiên làm việc tiếp theo.
Claude Code
mkdir -p ~/.claude/skills && curl -fsSL https://skill.dn1nguoi.com/z/frontend-data-contracts-61df0.tgz | tar xz -C ~/.claude/skills
Gọi trong Claude Code: /frontend-data-contracts – rồi mô tả công việc bạn muốn làm.
Codex
mkdir -p ~/.codex/skills && curl -fsSL https://skill.dn1nguoi.com/z/frontend-data-contracts-61df0.tgz | tar xz -C ~/.codex/skills
Gọi trong Codex: $frontend-data-contracts – rồi mô tả công việc bạn muốn làm.
Hermes Agent
mkdir -p ~/.hermes/skills && curl -fsSL https://skill.dn1nguoi.com/z/frontend-data-contracts-61df0.tgz | tar xz -C ~/.hermes/skills
Gọi trong Hermes Agent: /frontend-data-contracts – rồi mô tả công việc bạn muốn làm.
DeepSeek
mkdir -p ~/.deepcode/skills && curl -fsSL https://skill.dn1nguoi.com/z/frontend-data-contracts-61df0.tgz | tar xz -C ~/.deepcode/skills
Gọi trong DeepSeek: /frontend-data-contracts – rồi mô tả công việc bạn muốn làm.
Nội dung SKILL.md
Nội dung SKILL.md gốc bằng tiếng Anh.
Frontend Data Contracts (typed network boundary)
When to Use
Use this skill when you need a portable, framework-agnostic discipline for type safety at the network edge of any React or React Native app. Establishes one typed API client as the single fetch boundary, a parse-don't-validate rule that turns wire JSON into trusted domain types before it enters the app, runtime parsing, normalized errors and branded identifiers.
> Portable skill — readable by Claude Code, OpenCode, Codex, Cursor, Windsurf, and others.
> This skill describes a discipline at the network edge — one client, one envelope, one error
> type, validated types — not a state library or a styling system. It pairs with the
> frontend-architecture skill (the client lives in shared/api-client/) and is the foundation
> the frontend-optimistic-mutations skill builds on.
The goal: the moment data crosses from the network into the app, it stops being any-shaped wire
JSON and becomes a trusted, typed domain value — or it becomes a single, typed error.
There is exactly one place this transformation happens, and nothing untyped escapes it.
---
Example contract
The snippets marked “essence” are architecture sketches with omitted project dependencies and methods, not a complete client. A generic get<Invoice> type parameter is only a compile-time assertion: it cannot establish runtime validity. Require the actual schema parse before treating a value as an Invoice, normalize parser failures into the project error type, and keep authorization independent of both schema validation and branded IDs. Bind adapter behavior to the installed library version; do not assume an unsupported per-query callback exists.
0. The five core ideas
1. One client is the only fetch boundary. A single typed apiClient wraps fetch. Components and hooks never call fetch/axios directly — the boundary is enforceable in review and lint.
2. Parse, don't validate. Wire JSON is parsed into domain types at the boundary. After the client returns, the value is trusted everywhere downstream — no defensive ?. chains, no re-checking shapes in components.
3. One envelope. Every response is { data } on success or { error } on failure. The client unwraps data and throws on error, so callers receive the payload directly or a typed throw.
4. One normalized error type. Server error envelope, non-2xx status, malformed body, network failure, and abort all become a single ApiError with a machine code, status, and optional per-field errors. Callers handle one shape.
5. Identifiers are branded. Domain IDs are nominal types (InvoiceId, CustomerId) so the compiler rejects passing one where another is expected — the most common silent bug in data-heavy UIs.
---
1. Directory layout
The boundary is one folder in shared/ (per the frontend-architecture skill).
src/shared/api-client/
├── index.ts ← barrel: apiClient, ApiError, types
├── client.ts ← the fetch wrapper: buildUrl, headers, parse, verbs
├── config.ts ← base URL resolution, default headers
├── error.ts ← the ApiError class + code→message-key mapping
├── types.ts ← envelope types, HttpMethod, RequestOptions, field errors
└── client.test.ts ← boundary behavior tests (envelope, errors, network)Domain entity types and their schemas live with their feature module
(modules/{feature}/types/) or a shared contract package; the client is generic over T.
---
2. One client, the only fetch boundary
Every verb returns the unwrapped data payload typed by the caller, and throws an
ApiError on any failure. Components never see envelopes or raw responses.
// shared/api-client/client.ts (essence)
export const apiClient = {
get<T>(path: string, options?: RequestOptions): Promise<T> {
return request<T>("GET", path, undefined, options);
},
post<T>(path: string, body?: unknown, options?: RequestOptions): Promise<T> {
return request<T>("POST", path, body, options);
},
patch<T>(path: string, body?: unknown, options?: RequestOptions): Promise<T> {
/* … */
},
put<T>(path: string, body?: unknown, options?: RequestOptions): Promise<T> {
/* … */
},
delete<T>(path: string, options?: RequestOptions): Promise<T> {
/* … */
},
} as const;
export type ApiClient = typeof apiClient;// CORRECT — a feature hook wraps the client, typed by the caller
const invoice = await apiClient.get<Invoice>(`/invoices/${id}`, { signal });
// WRONG — a raw fetch in a component bypasses the boundary entirely
const res = await fetch(`/api/invoices/${id}`); // untyped, unhandled errors, no envelopeHard rules:
- No
fetch/axios/XMLHttpRequestoutsideshared/api-client/— enforce with an ESLintno-restricted-imports/no-restricted-globalsrule. - The client is framework-free: no toasts, no router, no React. Side effects (toasts, redirects) live in the query layer's
onError(see §6). - Pass
AbortSignalthroughRequestOptionsso the query layer can cancel (wired by TanStack Query).
---
3. Parse, don't validate (the boundary transform)
"Validate" leaves you with the same untyped value and a boolean. "Parse" returns a **new, typed
value** — so downstream code is guaranteed correct by the type system. Run a schema parse at the
boundary; after that, the value is trusted.
// modules/invoice/types/invoice.schema.ts
import { z } from "zod";
export const invoiceSchema = z.object({
id: z.string().transform(toInvoiceId), // brand it (see §5)
number: z.string(),
status: z.nativeEnum(InvoiceStatus),
total: z.number().int(), // minor units — never float money
issuedAt: z.string().datetime(),
});
export type Invoice = z.infer<typeof invoiceSchema>;// the client (or a thin per-entity wrapper) parses at the edge
const raw = await apiClient.get<unknown>(`/invoices/${id}`, { signal });
return invoiceSchema.parse(raw); // throws on contract drift → surfaces as a typed failureWhy this matters: a backend that renames a field or sends a null it shouldn't is caught **at
the boundary**, with a clear error, instead of producing undefined three components deep where
the stack trace is useless. Components downstream never write invoice?.total ?? 0 defensively.
> Validation library is your choice — Zod, Valibot, ArkType, io-ts. The rule is
> constant: a parse step converts unknown wire data into a typed domain value at one boundary.
---
4. One response envelope
Mirror the backend's single envelope in the client and unwrap it once.
// shared/api-client/types.ts
export interface ApiSuccessEnvelope<T> {
data: T;
}
export interface ApiErrorEnvelope {
error: ApiErrorBody;
}
export type ApiEnvelope<T> = ApiSuccessEnvelope<T> | ApiErrorEnvelope;
export function isApiErrorEnvelope<T>(
e: ApiEnvelope<T>,
): e is ApiErrorEnvelope {
return typeof e === "object" && e !== null && "error" in e;
}
export interface ApiErrorBody {
code: ServerErrorCode; // machine-readable, stable
message: string; // server message (NOT shown to users directly)
fields?: Record<string, string[]>; // per-field validation errors
}The parse step handles every shape: 204 No Content → undefined; { error } → throw; non-2xx
with no well-formed envelope → synthesize an error; { data } → return data. The caller only
ever sees a typed payload or a throw.
---
5. Branded (nominal) identifiers
Strings are interchangeable; domain IDs are not. Brand them so the compiler stops you passing a
CustomerId where an InvoiceId is required.
// shared/types/id.ts
declare const brand: unique symbol;
export type Brand<T, B extends string> = T & { readonly [brand]: B };
export type InvoiceId = Brand<string, "InvoiceId">;
export type CustomerId = Brand<string, "CustomerId">;
export const toInvoiceId = (s: string): InvoiceId => s as InvoiceId;
export const toCustomerId = (s: string): CustomerId => s as CustomerId;function loadInvoice(id: InvoiceId) {
/* … */
}
loadInvoice(customerId); // ❌ compile error — exactly the bug you want caught
loadInvoice(invoiceId); // ✅Brand IDs at the parse boundary (§3) so every ID in the app is already nominal. The runtime cost is
zero — brands erase at compile time.
---
6. One normalized error type
Collapse every failure mode into a single ApiError so callers handle one shape. It carries a
machine code, the HTTP status, optional per-field errors, and a stable key for localized
messages (it does not localize — that's the UI's job).
// shared/api-client/error.ts (essence)
export class ApiError extends Error {
readonly code: ServerErrorCode;
readonly status: number; // 0 when no response (network/abort)
readonly fields?: Record<string, string[]>;
get isNetworkError() {
return this.status === 0;
}
get hasFieldErrors() {
return !!this.fields && Obj… (nội dung đầy đủ có trong gói tải về)
Skill liên quan
- frontend-design – Thiết kế và triển khai giao diện frontend nổi bật trong khuôn khổ thương hiệu, khả năng…
- frontend-dev-guidelines – Bạn là kỹ sư frontend cấp cao, làm việc theo tiêu chuẩn kiến trúc và hiệu năng nghiêm…
- frontend-lighthouse – Thêm cổng Lighthouse CI có thể tái sử dụng cho bản build frontend production, với ngân…
- frontend-mobile-development-component-scaffold – Khởi tạo khung component React và React Native với TypeScript, khả năng truy cập, test…
- frontend-observability – Mẫu quan sát (observability) phía client độc lập framework, có thể tái sử dụng cho tích…
- frontend-optimistic-mutations – Kỷ luật độc lập framework cho luồng ghi (write path) của mọi ứng dụng React hoặc React…
- frontend-seo – Hệ thống SEO độc lập framework, dùng được cho mọi frontend React hoặc React…
- frontend-ui-dark-ts – Hệ thống giao diện React theo chủ đề tối hiện đại, dùng Tailwind CSS và Framer Motion…