| name | code-quality |
| description | Production-grade code quality enforcement for TypeScript, Next.js, Python, and FastAPI. This skill MUST be loaded when ANY .ts, .tsx, .py, .jsx, or .js file is being written, edited, reviewed, or created. Also trigger when the user asks to "create a component", "build a page", "add a feature", "write a function", "create an endpoint", "add a route", "implement a service", "review code", "refactor", "clean up", "improve code quality", "fix code", or any task that produces code output. This skill prevents the most common Claude failures: dumping all code into a single file, using function keyword instead of arrow functions, skipping error handling, missing type safety, writing insecure code, and creating untestable monolithic functions. Load this skill BEFORE writing any code — it governs structure, not just syntax. |
Production Code Quality
This skill enforces production-grade code quality across TypeScript/Next.js and Python/FastAPI. It targets the specific ways Claude fails when generating code — not theoretical best practices, but the actual mistakes that ship broken, unmaintainable, or insecure code to production.
Language-specific rules: When working on TypeScript/Next.js files, read references/typescript-quality.md. When working on Python/FastAPI files, read references/python-quality.md. For security patterns across both, read references/security-checklist.md.
Rule #1: File Organization (The Single-File Problem)
Claude's worst habit is stuffing interfaces, constants, helper functions, schemas, API calls, sub-components, and the actual logic all into one file. This creates 400+ line monoliths that are untestable, unreusable, and unmaintainable.
Before Creating ANY File, Ask:
-
Does this project already have a types/, lib/, utils/, constants/, schemas/, or services/ directory? If yes, use it. Match the existing project structure — never invent a new organization pattern when one exists.
-
Am I about to put more than one "kind" of code in this file? Types are one kind. Constants are another. API calls are another. The component/handler is another. Each kind gets its own file.
TypeScript / Next.js File Map
When building a feature, create these dedicated files:
feature/
├── page.tsx (or component.tsx) ← ONLY the component JSX + hooks
├── types.ts ← All interfaces, types, enums
├── constants.ts ← Magic values, config objects, labels
├── schemas.ts ← Zod validation schemas
├── actions.ts ← Server actions (Next.js)
├── services/
│ └── feature-service.ts ← API calls, data fetching
├── hooks/
│ └── use-feature.ts ← Custom hooks
├── utils.ts ← Helper/transform functions
└── components/
└── sub-component.tsx ← Extracted sub-components (>30 lines)
What goes where:
| Code Type | Dedicated File | Extract When |
|---|
| Interfaces & types | types.ts | Always if >2 types OR any type is used by another file |
| Constants & config | constants.ts | Always if >1 constant OR any is reusable |
| Zod schemas | schemas.ts | Always — schemas are never inline |
| Server actions | actions.ts | Always — 'use server' in its own file |
| API calls / fetching | services/feature-service.ts | Always |
| Custom hooks | hooks/use-feature.ts | Always |
| Utility functions | utils.ts | If >1 function OR any is reusable |
| Sub-components | components/sub.tsx | If >30 lines OR used in multiple places |
Python / FastAPI File Map
feature/
├── router.py ← Route handlers ONLY (thin layer)
├── schemas.py ← Pydantic request/response models
├── models.py ← SQLAlchemy / ORM models
├── service.py ← Business logic
├── dependencies.py ← FastAPI Depends() functions
├── constants.py ← Magic values, config
├── exceptions.py ← Custom exception classes
└── utils.py ← Helper functions
What goes where:
| Code Type | Dedicated File | Extract When |
|---|
| Pydantic schemas | schemas.py | Always — never inline in router |
| ORM models | models.py | Always |
| Business logic | service.py | Always — routers call services, not the other way |
| FastAPI dependencies | dependencies.py | Always if shared across routes |
| Custom exceptions | exceptions.py | If >1 exception class |
| Constants | constants.py | Always if >1 constant |
| Utilities | utils.py | If reusable |
The Split Decision
For small, self-contained code (under ~100 lines total, no reusable parts), keeping everything in one file is acceptable. But the moment you have:
- More than 2 type definitions → extract to
types.ts / schemas.py
- More than 1 constant → extract to
constants.ts / constants.py
- Any function used by another file → extract to
utils.ts / utils.py
- Any API call → extract to
services/ or service.py
- Any file exceeding 150 lines → split by responsibility
If in doubt, split. Merging files is easy. Untangling a 500-line monolith is not.
Rule #2: Arrow Functions (TypeScript)
Use arrow functions for everything in TypeScript. Never use the function keyword.
function UserTable({ users }: UserTableProps) {
return <div>...</div>
}
function formatDate(date: Date): string {
return date.toLocaleDateString()
}
function useUsers() {
const [users, setUsers] = useState<User[]>([])
return { users }
}
const UserTable = ({ users }: UserTableProps) => {
return <div>...</div>
}
const formatDate = (date: Date): string => {
return date.toLocaleDateString()
}
const useUsers = () => {
const [users, setUsers] = useState<User[]>([])
return { users }
}
This applies to:
- React components (named exports:
export const Component = () => {})
- Custom hooks
- Helper/utility functions
- Event handlers
- Service functions
- Callbacks and inline functions
Exception: export default pages in Next.js App Router may use export default function Page() when required by the framework, but prefer named arrow exports when possible.
Rule #3: Pre-Write Checklist
Before writing ANY code, answer these questions:
1. What files do I need to create?
Map out the files FIRST. Never start writing code in a single file and "figure out the structure later." Refer to the file maps above. Check what directories already exist in the project.
2. What can go wrong?
For every function that does I/O (API call, database query, file read), plan the error path:
- What exception/error can this throw?
- What does the user see when it fails?
- Is there a retry or fallback?
3. What types do I need?
Define interfaces and types BEFORE writing the implementation. If you're reaching for any, stop and design the type. If you're using inline { name: string; email: string } more than once, extract it to types.ts.
4. What already exists that I can reuse?
Before writing a new utility function, check if the project already has:
- A similar function in
utils/, lib/, or helpers/
- A shared type that covers this use case
- A service/hook that already fetches this data
- A constant file with values you're about to hardcode
Rule #4: Post-Write Checklist
After writing code, verify against these gates:
TypeScript / Next.js
□ No `any` types — every variable, parameter, and return has a specific type
□ No `function` keyword — arrow functions everywhere
□ No inline interfaces used more than once — extracted to types.ts
□ No hardcoded strings/numbers — extracted to constants.ts
□ No API calls in components — extracted to services/ or actions.ts
□ No Zod schemas inline — extracted to schemas.ts
□ No floating promises — every async call has await or proper error handling
□ No useEffect with missing/incorrect dependencies
□ No inline objects/functions in JSX props causing re-renders
□ Server/client boundary is correct ('use client' only where needed)
□ Error states handled — not just the happy path
□ Loading states exist for async operations
□ Each file has a single responsibility
□ Each file is under 200 lines (stretch limit: 300)
Python / FastAPI
□ Every function has type hints on parameters AND return type
□ No bare except: — always catch specific exceptions
□ No print() — use logging module
□ No blocking I/O in async def — use await or asyncio.to_thread()
□ No string-formatted SQL — use parameterized queries
□ Pydantic schemas in schemas.py, not in router.py
□ Business logic in service.py, not in router.py
□ Route handlers are thin — validate input, call service, return response
□ Custom exceptions defined and used (not raw HTTPException everywhere)
□ No hardcoded secrets — use environment variables
□ Each file has a single responsibility
□ Each file is under 200 lines (stretch limit: 300)
Common Mistakes Claude Makes (And How to Fix Them)
1. The Monolith File
What Claude does: Puts types, constants, helpers, schemas, API calls, and the component all in one file.
Fix: Before writing the first line, list the files you need. Create them all. Then write code into the correct file.
2. Using any When Types Are Unclear
What Claude does:
const handleResponse = (data: any) => { ... }
const config: any = { ... }
Why it's dangerous: any silently disables type checking for everything it touches. One any in a function signature means every caller loses type safety.
Fix:
interface ApiResponse {
users: User[]
pagination: PaginationMeta
}
const handleResponse = (data: ApiResponse) => { ... }
If the type is genuinely unknown at write time, use unknown and narrow with type guards.
3. Using function Instead of Arrow Functions
What Claude does:
function calculateTotal(items: CartItem[]): number { ... }
export default function Dashboard() { ... }
Fix:
const calculateTotal = (items: CartItem[]): number => { ... }
const Dashboard = () => { ... }
export default Dashboard
4. No Error Handling on API Calls
What Claude does:
const users = await fetch("/api/users").then(r => r.json())
Fix:
try {
const res = await fetch("/api/users")
if (!res.ok) throw new Error(`Failed to fetch users: ${res.status}`)
const users: User[] = await res.json()
} catch (error) {
}
5. Blocking I/O in Async Python
What Claude does:
async def get_report(report_id: str):
with open(f"reports/{report_id}.pdf", "rb") as f:
return f.read()
Why it's dangerous: A single blocking call in an async function freezes ALL concurrent requests in the event loop.
Fix:
import asyncio
async def get_report(report_id: str):
return await asyncio.to_thread(_read_report, report_id)
def _read_report(report_id: str) -> bytes:
with open(f"reports/{report_id}.pdf", "rb") as f:
return f.read()
6. Bare except: That Swallows Everything
What Claude does:
try:
result = process(data)
except:
pass
Fix:
try:
result = process(data)
except ValidationError as e:
logger.warning("Invalid data: %s", e)
raise HTTPException(status_code=422, detail=str(e))
except ExternalServiceError as e:
logger.error("Service unavailable: %s", e)
raise HTTPException(status_code=503, detail="Service temporarily unavailable")
7. Hardcoded Magic Values
What Claude does:
if (user.role === "admin") { ... }
const timeout = 5000
if (items.length > 50) { ... }
Fix:
export const USER_ROLES = {
ADMIN: "admin",
USER: "user",
MODERATOR: "moderator",
} as const
export const API_TIMEOUT_MS = 5000
export const MAX_ITEMS_PER_PAGE = 50
8. Inline Type Assertions Instead of Type Guards
What Claude does:
const user = data as User
Fix:
const isUser = (data: unknown): data is User => {
return (
typeof data === "object" &&
data !== null &&
"id" in data &&
"email" in data
)
}
if (isUser(data)) {
}
9. God Functions (100+ Lines)
What Claude does: Writes a single function that validates input, fetches data, transforms it, handles errors, and formats output.
Fix: Each function does ONE thing. Extract steps into named functions with clear inputs/outputs:
const processOrder = async (input: unknown) => {
}
const processOrder = async (input: unknown) => {
const validated = validateOrderInput(input)
const enriched = await enrichWithPricing(validated)
const order = await saveOrder(enriched)
await notifyCustomer(order)
return formatOrderResponse(order)
}
10. Missing Return Types
What Claude does:
const getUser = async (id: string) => {
const res = await db.users.findUnique({ where: { id } })
return res
}
Fix:
const getUser = async (id: string): Promise<User | null> => {
const res = await db.users.findUnique({ where: { id } })
return res
}
The Line Count Rule
If any single file exceeds 200 lines, something is wrong. Either:
- Multiple responsibilities are mixed → split into dedicated files
- A function is too long → extract sub-functions
- Too many types are inline → move to
types.ts
Hard ceiling: 300 lines. If a file reaches 300, it MUST be split before proceeding. No exceptions.