Naming & File Organization
Names are 60% of code quality. If I have to read the function body to know what it does, the name has failed.
File naming
| Kind | Convention | Example |
|---|---|---|
| Components | PascalCase.tsx |
LoginForm.tsx |
| Hooks | use-kebab-case.ts |
use-auth.ts |
| Services / utils | kebab-case.ts |
auth-service.ts |
| Types | kebab-case.ts |
user.ts |
| Tests | *.test.ts(x) |
LoginForm.test.tsx |
| Routes (Expo / Next) | framework defaults | (tabs)/index.tsx |
I don’t fight the framework. Expo Router and Next.js App Router have opinions about route filenames — I follow them, no matter what the rest of the project looks like.
Variable & function naming
- Booleans start with
is,has,should,can→isLoading,hasAccess,shouldRetry - Functions start with a verb →
getUser,fetchOrders,formatDate - Event handlers start with
handlefor definitions,onfor props →handleSubmitdefined,onSubmitas a prop name - Async actions end with the noun, not “promise” or “async” →
loadUser, notloadUserAsync - Constants are
SCREAMING_SNAKE_CASEonly when truly constant (env-derived or magic numbers). Module-level config objects staycamelCase.
Folder structure — feature-first, always
src/
├── app/ # Routing only — thin
├── features/
│ ├── auth/
│ │ ├── components/ # Feature-local UI
│ │ ├── hooks/ # Feature-local hooks
│ │ ├── services/ # API calls, business logic
│ │ ├── store/ # Zustand slice for this feature
│ │ ├── @types/ # Feature-local types
│ │ └── index.ts # Public API — what the rest of the app can import
│ ├── chat/
│ └── orders/
└── global/
├── components/ # Truly shared UI (Button, Input, Modal)
├── hooks/ # Cross-cutting hooks (useDebounce, useMediaQuery)
├── utils/
├── store/ # App-wide stores (theme, auth-session)
├── config/
└── @types/
Three hard rules
- Screens are thin. A route file imports feature components and composes them — no fetching, no business logic, no Zustand calls.
- Features don’t cross-import. If
chatneeds something fromauth, that something belongs inglobal/. Otherwise you’ve just built a graph of hidden coupling. - Every feature exposes a barrel. Consumers import from
@/features/auth, never from@/features/auth/components/LoginForm. The barrel is the feature’s contract.
Imports
- Absolute imports via
@/alias — never../../../ - Order:
react/ framework → third-party →@/global→@/features→ relative → styles - One blank line between groups; many editors enforce this automatically
import { useState } from "react";
import { useQuery } from "@tanstack/react-query";
import { Button } from "@/global/components";
import { useAuthStore } from "@/features/auth";
import { useChatScroll } from "./hooks/use-chat-scroll";
import styles from "./ChatWindow.module.css";
What goes in global/
A new file goes in global/ only when two or more features need it. Speculative “we’ll probably need this everywhere” code stays inside the feature that first uses it. Move it up the day a second feature reaches for it.