Naming and imports
File names that say what's inside, absolute imports, and no barrel files.
File names
The name tells you what kind of file it is before you open it.
| Kind | Pattern | Example |
|---|---|---|
| Component | PascalCase.tsx | ProjectCard.tsx |
| Hook | use-name.ts | use-update-profile-mutation.ts |
| Schema | name.schema.ts | login.schema.ts |
| API | name.api.ts | auth.api.ts |
| Query options | name.query-options.ts | profile.query-options.ts |
| Store | name-store.ts | auth-store.ts |
| Types | name.types.ts | profile.types.ts |
| Constants | UPPER_SNAKE_CASE export | QUERY_KEYS |
One exception
Feature and shared components use PascalCase. UI primitives in
components/ui/ stay kebab-case (button.tsx), like hooks and utilities.
Biome enforces the split with a filename rule override; see
Biome and Ultracite.
Absolute imports
Use the @/ alias for anything outside the current folder:
import { Button } from '@/components/ui/button';../../../components/ui/button breaks the moment a file moves, and it's hard
to read.
No barrel files
No index.ts files that re-export a folder. Import from the file that
defines the thing:
// Good
import { loginSchema } from '@/features/auth/schemas/login.schema';
// Bad
import { loginSchema } from '@/features/auth';Why:
- No circular imports. Barrels make it easy for two features to import each other through their index files.
- Easier to find things. The import path is the file path.
- Smaller bundles. The bundler doesn't have to pull in a whole folder to get one export.
TypeScript
- Strict mode, and no
any. When a type is truly unknown, useunknownand narrow it. - Prefer
interfacefor object shapes, and discriminated unions for values that can be one of several shapes. - Let TypeScript infer what it can. Annotate exported function boundaries, not every local variable.
- Exported utilities get a JSDoc comment that says what they return.
Last updated on