Engineering standards
Skill vmvenkatesh78/engineering-skills/skills/engineering-standards
Nine Claude Agent Skills that encode production engineering discipline. Operating bar, code review loops, design constraints, runtime concerns. Platform-agnostic; written for the Agent Skills format but the rules are engineering judgment, not vendor lock-in.
npx -y skills add vmvenkatesh78/engineering-skills --skill engineering-standardsAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Complete frontend engineering standards for code quality, React patterns, TypeScript, CSS architecture, accessibility, testing, performance, security, and code review. Apply to every code task, writing, reviewing, debugging, refactoring. These rules are non-negotiable and override convenience or speed.
SKILL.md
22.0 KB, as published. Nobody here has run it
Frontend Engineering Standards
These standards apply to every line of code written, reviewed, or refactored. No exceptions for prototypes, demos, or "quick fixes." Code that violates these rules does not ship.
FOUR-STATE RULE: EVERY ASYNC OPERATION
AI generates code that works when everything goes right. Production code handles when things go wrong.
Every fetch, API call, or async operation MUST handle four states:
- Loading. Skeleton or spinner visible before data arrives
- Error. Meaningful error message, retry mechanism, or fallback
- Empty. Explicit empty state with action to resolve it
- Success. The data renders correctly
If code only handles success, it is not production-ready. Reject it on review.
// WRONG, happy path only
const { data } = await fetchUsers();
return <UserList users={data} />;
// RIGHT: all four states
function UserList() {
const { data, error, isLoading } = useUsers();
if (isLoading) return <Skeleton rows={5} />;
if (error) return <ErrorState message={error.message} onRetry={refetch} />;
if (!data?.length) return <EmptyState action={<CreateUserButton />} />;
return <Table rows={data} />;
}
TYPESCRIPT
Strict Mode: Non-Negotiable
Every project: strict: true, noUncheckedIndexedAccess: true, exactOptionalPropertyTypes: true.
Forbidden Patterns
any, never. Useunknownand narrow with type guards.astype assertions, only with proof (instanceof check, discriminated union).@ts-ignore/@ts-expect-errorwithout a comment explaining why.- Non-null assertion (
!), use optional chaining or explicit guards. enum, useas constobjects instead. Enums have runtime cost and quirky behavior.
// WRONG
const value = data as UserResponse;
const name = user!.name;
enum Status { Active, Inactive }
// RIGHT
function isUserResponse(data: unknown): data is UserResponse {
return typeof data === 'object' && data !== null && 'id' in data;
}
const name = user?.name ?? 'Unknown';
const Status = { Active: 'active', Inactive: 'inactive' } as const;
Required Patterns
- All function parameters:
Readonly<>when the function should not mutate them. - All component props: defined as interfaces, not inline types.
- All re-exports:
export type { X }for type-only exports (isolatedModules compliance). - Generic components: explicit constraint + default:
<T extends ElementType = 'button'>. - Discriminated unions for state machines, not boolean flags.
// WRONG, boolean flags
interface State {
isLoading: boolean;
isError: boolean;
data: User[] | null;
error: Error | null;
}
// RIGHT, discriminated union
type State =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'error'; error: Error }
| { status: 'success'; data: User[] };
REACT PATTERNS
Component Architecture
- One component per file. Component name matches filename.
- Compound components over prop-heavy monoliths (>8 props = split).
- Hooks extract reusable logic. Components own behavior, styles own presentation.
forwardRefcomponents MUST havedisplayName.- No inline styles except truly dynamic values (computed positions, API-driven colors).
- No class components. Functional only.
Hooks Rules
useEffectwith async work: ALWAYS include cleanup (AbortController or stale flag).useEffectdependency arrays: exhaustive or line-level eslint-disable with rationale.useMemo/useCallback: only when profiling shows measurable gain or reference stability matters.useState: prefer single state object for related state that updates together.useRef: for DOM refs, abort controllers, values that don't trigger re-renders.- Never call
setStateduring render, derive withuseMemo. - Never nest hooks inside conditions or loops.
// WRONG, no cleanup, race condition
useEffect(() => {
fetchData().then(setData);
}, [id]);
// RIGHT: abort controller, cleanup, error handling
useEffect(() => {
const controller = new AbortController();
async function load() {
try {
const result = await fetchData(id, { signal: controller.signal });
setData(result);
} catch (err) {
if (!controller.signal.aborted) {
setError(err instanceof Error ? err : new Error('Unknown error'));
}
}
}
load();
return () => controller.abort();
}, [id]);
State Management
- Co-locate state where it's used. Don't hoist to global store unless 2+ unrelated components need it.
- No derived state in store, compute with selectors or
useMemo. - Context: use for dependency injection (theme, auth), not for frequently changing state.
- Async operations: dispatch started/success/failure actions. Never leave error paths unhandled.
Props
- Boolean props:
is/has/allow/enableprefix:isLoading,hasError. - Event handlers:
handleprefix for component handlers,onprefix for prop callbacks:handleClick,onClick. - No boolean parameters in functions, use options objects instead.
// WRONG
function createUser(name: string, isAdmin: boolean, sendEmail: boolean) {}
// RIGHT
function createUser(name: string, options: { isAdmin?: boolean; sendEmail?: boolean }) {}
CSS ARCHITECTURE
Design System CSS
- Token-driven: every color, spacing, font size, radius, and shadow comes from CSS custom properties.
- No hardcoded hex values, pixel values, or font names in component CSS.
- Option B pattern: base selector declares structure with
--_intermediate variables, variant selectors swap values. - Data attribute selectors:
[data-variant="primary"], not class-based variants. - Plain CSS files with data attributes, no CSS-in-JS, no CSS Modules, no Tailwind in component library code.
CSS Rules
- No
!importantunless overriding third-party styles (with comment). - No element-type selectors deeper than 2 levels.
&:focus-visiblenot&:focusfor keyboard-only indicators.flex-shrink: 0on icons and fixed-width elements in flex containers.- No
:global()selectors targeting library internals. - Transitions: only on
color,background-color,border-color,opacity,transform,box-shadow. Never onpadding,margin,width,height,border-radius. - Duration: 100-150ms for hover/active, 200ms for modals. No bounce/spring/elastic easing.
Production UI Standard Compliance
All UI output must comply with the production-ui-standard skill. Key checks:
- Primary accent: blue (#2563EB light, #3B82F6 dark). Not indigo, not purple.
- Grays: Slate family (cool-toned). Not neutral gray.
- Shadows: use Slate-900 rgba, not pure black. Cards on white = border, no shadow.
- Border radius: 6px buttons/inputs, 8px cards, 12px modals. Never above 12px on rectangles.
- No gradients on buttons. No glassmorphism. No colored card backgrounds.
ACCESSIBILITY: NON-NEGOTIABLE
- Every interactive element: keyboard-reachable and operable.
- Every icon-only button:
aria-label. - Every image:
alttext (emptyalt=""for decorative). tabIndex> 0: NEVER.- Color as sole indicator: NEVER. Always pair with text, icon, or pattern.
role="button"on non-button elements: MUST also havetabIndex={0}andonKeyDown.- Focus indicators: 2px solid outline, 2px offset, visible on EVERY interactive element.
- Contrast: 4.5:1 normal text, 3:1 large text minimum. Target 7:1 where possible.
- Touch targets: 44x44px minimum on mobile.
- Reduced motion:
@media (prefers-reduced-motion: reduce)disables all animations. - Screen reader: all form inputs have associated labels, all regions have landmarks.
- Focus traps: only in modals/dialogs. Must be escapable with Escape key.
ERROR HANDLING
- No empty
catchblocks: at minimum log with context or re-throw. - API errors: distinguish 4xx (user error) from 5xx (server error) in UI messaging.
- Form validation: show inline errors, not just toasts.
- Network errors: provide retry mechanism or clear feedback.
Promise.all: handle errors, one rejection must not crash the entire batch. UsePromise.allSettledwhen partial success is acceptable.- Every
switchstatement:defaultcase required. - Null/undefined from API: never assume shape matches TypeScript interface at runtime. Validate.
// WRONG: assumes API shape
const userName = response.data.user.name;
// RIGHT, defensive
const userName = response.data?.user?.name ?? 'Unknown';
PERFORMANCE
- No O(n²) operations on arrays that could exceed 100 items.
- No
new RegExp()inside render, extract to module scope oruseMemo. keyprops: stable identifiers, never array indices (unless static, never reordered).- Lazy load routes and heavy components:
React.lazy+Suspense. - No layout thrashing: batch DOM reads before DOM writes.
- Images: use
loading="lazy"for below-fold images, specifywidth/heightto prevent CLS. - Bundle: code-split at route level minimum. Monitor with build:size script.
- Memoization: profile first. Don't
useMemoeverything, measure before optimizing.
SECURITY
- No API keys, tokens, or secrets in source code.
- No PII in console.log statements.
- All user inputs sanitized before rendering.
dangerouslySetInnerHTML: ONLY with DOMPurify sanitization. Prefer avoiding entirely.eval(),new Function(),innerHTMLwith user content: FORBIDDEN.- File uploads: validate extensions AND file size. Use allowlist, not blocklist.
- CSS custom properties from user input: block
url(),@import,expression(). - Third-party scripts: load with
integrityattribute (SRI) when available. - CORS: never use
Access-Control-Allow-Origin: *in production.
TESTING
Structure
- Test file: adjacent to source:
Component.test.tsx. - Test names: describe behavior, not implementation:
"shows error when API returns 400"not"calls setError". - No test depends on another test's state.
- Mock at boundaries (API calls, browser APIs), not internal functions.
Coverage
- Every component: at least one test for render, one for interaction, one for error state.
- Every hook: test the contract (inputs → outputs), not the implementation.
- Every async operation: test both success and error paths.
- Every form: test validation, submission, and error display.
- Edge cases: empty data, null values, extremely long strings, special characters.
Patterns
// Test the four states
describe('UserList', () => {
it('shows skeleton while loading', () => { /* ... */ });
it('shows error message when fetch fails', () => { /* ... */ });
it('shows empty state when no users exist', () => { /* ... */ });
it('renders user rows on success', () => { /* ... */ });
});
- Accessibility tests: use
jest-axeorvitest-axefor automated a11y checks. - No
data-testidas the primary query strategy, prefergetByRole,getByLabelText,getByText. - Test user behavior, not implementation details. Click buttons, type in inputs, verify visible output.
NAMING CONVENTIONS
Files
- Components:
PascalCase:FormUpload.tsx - Hooks:
camelCasewithuseprefix:useClickOutside.ts - Utils:
camelCase:formatDate.utils.ts - Tests:
*.test.tsx/*.test.ts - Styles:
*.css(design system),*.module.scss(app-level) - Constants:
SCREAMING_SNAKE_CASEin files,camelCasefilenames
Code
- Components:
PascalCase - Hooks:
useCamelCase - Functions/variables:
camelCase - Constants:
SCREAMING_SNAKE_CASE - Types/Interfaces:
PascalCase:interface ButtonProps - Generic type params: single uppercase letter or descriptive:
T,TElement - Boolean:
is/has/should/canprefix - Event handlers:
handleprefix (component),onprefix (prop)
GIT CONVENTIONS
- Commit format:
type(scope): description - Types:
feat,fix,refactor,style,perf,docs,test,chore,build,ci,revert - One logical change per commit.
- Remove all
console.logbefore committing. - No commented-out code, git has history.
- PR: one concern per PR. Don't mix features with refactors.
FILE SIZE LIMITS
- Components: <300 lines. Split if larger.
- Functions: <40 lines. Extract helpers if larger.
- Max nesting: 3 levels. Invert conditions or extract.
- Utils files: split when exceeding 500 lines.
- CSS files: one file per component. No monolithic stylesheets.
FORBIDDEN PATTERNS: REJECT ON SIGHT
| Pattern | Why | Fix |
|---|---|---|
any | Defeats type safety entirely | unknown + type guard |
as without proof | Lies to the compiler | Runtime check first |
| Empty catch block | Swallows errors silently | Log with context or re-throw |
console.log in committed code | Debug artifact in production | Remove before commit |
!important without comment | Specificity war | Fix selector specificity |
eslint-disable at file level | Blanket suppression | Line-level with reason |
// TODO without ticket | Never gets done | // TODO(TICKET-123): desc |
| Magic numbers | Unreadable, unmaintainable | Named constants |
| Hardcoded API URLs | Environment coupling | Config constants |
index as React key | Causes bugs on reorder | Stable unique identifier |
| Derived state in store | Stale data, extra updates | Selector or useMemo |
setState during render | Infinite loop risk | useMemo for derivations |
useEffect without cleanup | Memory leak, race condition | AbortController or stale flag |
tabIndex > 0 | Breaks natural tab order | Restructure DOM order |
| Color as sole indicator | Accessibility failure | Add text/icon/pattern |
dangerouslySetInnerHTML no sanitize | XSS vulnerability | DOMPurify or avoid |
| Fetch with only success handling | Crashes on error/empty | Four-state pattern |
| Boolean function parameters | Unreadable call sites | Options object |
| Nested ternaries | Unreadable | if/else or early return |
| Prop drilling >3 levels | Maintenance burden | Context or composition |
REVIEW CHECKLIST: RUN ON EVERY REVIEW
Pre-flight (before reading code)
- Does the PR have a clear title in commit format?
- Is it one logical change or a mixed bag?
- Are there any
console.logstatements?
Code Quality
- TypeScript strict, no
any, no unsafeas, no! - Every async operation handles four states (loading, error, empty, success)
- Every
useEffectwith async work has cleanup - Every error path has user-facing feedback
- No magic numbers, no hardcoded strings, no hardcoded URLs
- Functions under 40 lines, components under 300 lines
- Nesting under 3 levels
Accessibility
- Every interactive element keyboard-reachable
- Every icon-only button has
aria-label - Focus indicators visible on all interactive elements
- Color contrast meets 4.5:1 minimum
- No color-only state indicators
Security
- No secrets, tokens, or PII in source
- User input sanitized before rendering
- No
eval,innerHTML, or unsanitizeddangerouslySetInnerHTML
CSS
- No hardcoded colors, spacing, or font values
- Token-driven: all values from CSS custom properties
- No
!importantwithout justification - Focus-visible, not focus
- Compliant with production UI standard
Testing
- Tests exist for success, error, and edge cases
- Test names describe behavior
- No tests depend on other tests' state
- Queries use
getByRole/getByLabelText, notdata-testid
Performance
- No O(n²) on growing arrays
- No RegExp construction in render
- Stable keys on list items
- Lazy loading for routes and heavy components
CONFIDENCE THRESHOLD
When reviewing code, score each finding 0-100. Only surface findings at 85+ confidence. This prevents noise and focuses review on real issues.
Severity levels:
- Critical. Security vulnerability, data loss, crash. Must fix before merge.
- High. Logic bug, accessibility failure, missing error handling. Should fix.
- Medium. Performance issue, architectural concern, maintainability. Consider fixing.
- Low. Naming, style preference, minor improvement. Optional.
Verdict:
- Ready to Merge. No Critical or High issues
- Needs Attention. Medium issues found
- Needs Work. Critical or High issues found
THE DURABILITY LADDER FOR TYPE-LEVEL CONSTRAINTS
The rules above tell you what TypeScript patterns to use. This section tells you the meta-principle those patterns serve: every constraint that protects correctness should be encoded at the highest tier the type system supports, and only fall to a lower tier when the higher tier genuinely cannot carry it.
The ladder, ranked by ability to survive consumer turnover, codebase growth, and AI-agent-assisted refactoring:
- Type-level constraint. The compiler refuses to mix incompatible categories of data. Discriminated unions for state. Branded types for validated values. Exhaustiveness checks via
assertNeveron every switch over a union. Enforced at build time; no consumer can bypass without writing a cast that is visible in code review. - Runtime invariant. A contract check at a module boundary that fires when the assumption is violated, with a stack trace pointing at the violation site. Used when the constraint is mechanically checkable but only at runtime.
- Test as executing context. A regression test named for the constraint it protects, which fails in CI if someone changes the code without understanding the assumption. Documentation that runs.
- Name with semantic load.
MAX_BSA_REPORTABLE_TRANSFER_USDsurvives the deletion of any comment because the name itself carries the meaning.MAX_AMOUNTdoes not. Anonymous magic numbers are findings, not acceptable defaults. - Comment with link to source. The fallback when nothing higher works. Used for regulatory citations, links to incident post-mortems, vendor SLA references. Treated as a known-leaky mechanism; minimized.
A type-level constraint can be silently weakened only by a code change that produces a compile error. A runtime invariant can be silently weakened only by a code change that produces a test failure. A comment can be silently deleted by a "cleanup" PR. The ladder makes this asymmetry explicit and chooses accordingly.
BRANDED TYPES CARRY CONSTRAINTS PAST THE NARROWING
A discriminated union prevents consumers from accessing the narrowed data without checking the discriminant. It does not prevent consumers from extracting the narrowed data and passing the raw underlying type to downstream code. A branded type closes that gap.
type Brand<T, B> = T & { readonly __brand: B };
type ValidatedFile = Brand<File, 'ValidatedFile'>;
function uploadToS3(file: ValidatedFile): Promise<UploadResult> { ... }
// A raw File cannot be passed to uploadToS3.
// The type system enforces the validation boundary across consumer turnover.
The standard objection: someFile as ValidatedFile bypasses the boundary. That is the point. The cast is visible in code review; a reviewer can grep for as ValidatedFile and ask why this code thinks it has a validated file. The cast is not impossible; it is visible.
Use branded types for: input that has crossed a security or validation boundary, IDs of different domains that must not be confused (UserId vs OrderId), units that must not be mixed (Milliseconds vs Seconds), and any value where the type system can carry a constraint the consumer would otherwise have to remember. The dev.to article on file validation as a security boundary walks through the discipline applied to a specific worked example.
EXHAUSTIVENESS CHECKS ARE MANDATORY ON UNIONS
Every switch over a discriminated union ends with a default branch that calls assertNever:
function assertNever(value: never): never {
throw new Error(`Unhandled variant: ${JSON.stringify(value)}`);
}
switch (result.status) {
case 'valid': return handleValid(result);
case 'rejected': return handleRejected(result);
// ... other cases
default: return assertNever(result);
}
When a new variant is added, every consumer that omits it produces a compile error at the exact line where the new case must be handled. The compiler becomes the audit trail. Without exhaustiveness checking, new variants ship silently; consumers continue compiling and start returning undefined from the missed case. The cost of assertNever is two lines per switch. The cost of not adding it is incident response.
DECISION CONTEXT IN CODE-SPECIFIC FORMS
Decision context (the why behind a non-obvious choice) is encoded at the highest durability tier from the ladder above. Specific rules:
- A magic number without a name or test attached is a finding. Either it gets a semantic-loaded name, a regression test that protects it, or both.
- A retry, timeout, or threshold value tuned to an external constraint must reference the constraint in its name or in a one-line pointer to an ADR.
PAYMENT_TIMEOUT_AT_1_5X_PROVIDER_SLA_MScarries the math;PAYMENT_TIMEOUT_MSdoes not. - A regex or parsing pattern derived from a spec carries a one-line comment with the spec reference:
// per RFC 5322 section 3.4.1. Comments are the right mechanism for content that is irreducibly textual and external. - A branded type or discriminated union variant is itself decision context. The type's name and structure encode the reasoning for treating this data as a distinct category.
The rule: if a value or pattern in the code carries reasoning that the next reader cannot reconstruct from the local code alone, the reasoning is preserved at the highest available durability tier, or the code does not ship.
These standards extend the rules above with the discipline of where each rule sits on the durability ladder. The patterns from the prior sections (no any, strict flags, discriminated unions for state) are the implementation. The discipline in this section is the meta-principle those patterns serve.