TypeScript
Use names a developer can guess before searching. Component, hook, schema, query-key, route, and collection names appear in Storybook, stack traces, docs, and code review, so treat public names as contracts.
Core conventions
| Context | Convention | Example |
|---|---|---|
| Components and types | PascalCase | UserCreateDialog, PaginatedResponse<T> |
| Hooks | use + PascalCase | useCurrentUser |
| Functions and variables | camelCase | formatCurrency, currentUser |
| Zod schemas | camelCase + Schema | userCreateSchema |
| Constants and env vars | SCREAMING_SNAKE_CASE | MAX_PAGE_SIZE, DATABASE_URL |
| Component, story, and test files | kebab-case | user-create-dialog.test.tsx |
| npm packages | scoped kebab-case | @dmwd-io/design-system |
| npm scripts | kebab or colon namespace | test:unit |
React and TanStack
| Context | Convention | Example |
|---|---|---|
| Component symbol | PascalCase | UserCreateDialog |
| Component file | kebab-case | user-create-dialog.tsx |
| Event handler prop | on<Event> | onSubmit |
| Event handler implementation | handle<Event> | handleSubmit |
| Boolean prop | is, has, or should prefix | isLoading |
| Context and provider | <Name>Context, <Name>Provider | AuthContext, AuthProvider |
| Store hook | use<Domain>Store | useCartStore |
TanStack Query keys are arrays ordered broad to narrow. Keep one key factory per domain so invalidation is precise.
export const invoiceKeys = { all: ["billing", "invoice"] as const, lists: () => [...invoiceKeys.all, "list"] as const, list: (filters: InvoiceFilters) => [...invoiceKeys.lists(), filters] as const, detail: (id: string) => [...invoiceKeys.all, "detail", id] as const,};Astro
Astro filenames become routes and content contracts.
| Context | Convention | Example |
|---|---|---|
| Page route | kebab-case under src/pages/ | src/pages/about-us.astro |
| Dynamic route | bracket param | src/pages/invoices/[id].astro |
| Rest route | [...slug].astro | src/pages/docs/[...slug].astro |
| Layout file and symbol | kebab-case file, PascalCase symbol | base-layout.astro, BaseLayout |
| Collection | plural noun, kebab-case folder | src/content/blog-posts/ |
| Entry slug | kebab-case | 2026-06-launch.md |
Collection keys appear in every getCollection() call. Treat a rename as a breaking change.
Do and do not
| Do | Do not |
|---|---|
useCurrentUser | currentUserHook |
userCreateSchema | UserCreateZod |
UserCreateDialog symbol | userCreateDialog symbol |
user-create-dialog.tsx file | UserCreateDialog.tsx file |
handleSelect implementation | onSelect implementation |
invoiceKeys.detail(id) | inline query-key arrays everywhere |
src/pages/invoices/[id].astro | src/pages/invoices/id.astro |
Enforcement
Use TypeScript strictness, lint naming rules, and filename-case rules in CI. For repos on ESLint, @typescript-eslint/naming-convention and unicorn/filename-case cover most cases. For Biome repos, use the equivalent naming-convention rule.