docs: add Trellis planning and project specs

This commit is contained in:
2026-07-01 06:27:55 -07:00
parent 7f227c5f2a
commit aafd9caaac
349 changed files with 55801 additions and 0 deletions

View File

@@ -0,0 +1,315 @@
# Code Quality Guidelines
> Mandatory code quality rules for all Next.js full-stack applications.
---
## No Non-Null Assertions
**NEVER** use non-null assertions (`!`). They bypass TypeScript's null checking and lead to runtime errors.
```typescript
// FORBIDDEN
const name = user!.name;
const value = data!.items![0]!;
// REQUIRED - Use explicit checks
const user = getUser();
if (!user) {
throw new Error('User not found');
}
const name = user.name;
// REQUIRED - Use optional chaining with fallback
const value = data?.items?.[0] ?? defaultValue;
// REQUIRED - Use local variable after null check
const project = getProject(id);
if (!project) {
return { success: false, reason: 'Project not found' };
}
const projectName = project.name;
```
---
## No `any` Type
```typescript
// BAD
function process(data: any) { ... }
// GOOD - Use proper types
function process(data: ProcessInput) { ... }
// GOOD - Use unknown for truly unknown data
function parseJSON(input: string): unknown {
return JSON.parse(input);
}
// BAD - any in cache updates
queryClient.setQueryData(['users'], (old: any) => ...);
// GOOD - Properly typed cache updates
queryClient.setQueryData<UserListData>(['users'], (old) => {
if (!old) return old;
return { ...old, items: old.items.filter((u) => u.id !== deletedId) };
});
```
---
## No `@ts-expect-error` / `@ts-ignore`
```typescript
// FORBIDDEN
// @ts-expect-error - field exists at runtime
const value = user.customField;
// @ts-ignore
doSomething(invalidArg);
// REQUIRED - Fix the type issue at the source
// If a field exists at runtime but not in types, update the type definition.
doSomething(validArg);
```
---
## No `console.log`
Use structured logging instead of `console.log`. This applies to both frontend and backend code.
```typescript
// BAD
console.log('User created:', userId);
console.log('Error:', error);
// GOOD - Backend: use structured logger
logger.info('user_created', { userId });
logger.error('operation_failed', { error, operationId });
// GOOD - Frontend: remove debug logs before commit
// Use browser dev tools for debugging, not console.log
```
**Exception**: `console.warn` and `console.error` are acceptable in frontend code for development-time warnings that will not appear in production.
---
## Import Ordering
Organize imports in this order, separated by blank lines:
```typescript
// 1. Node built-ins
import path from 'node:path';
// 2. External packages
import { z } from 'zod';
import { useQuery } from '@tanstack/react-query';
// 3. Internal workspace packages
import type { User } from '@your-app/api/modules/users/types';
// 4. Local imports (relative paths)
import { formatDate } from './utils';
import type { Props } from './types';
```
Always use `import type` for type-only imports:
```typescript
// GOOD
import type { User, Project } from './types';
import { createUser } from './procedures';
// BAD - Mixed imports without type annotation
import { User, createUser } from './types';
```
---
## Naming Conventions
### Files and Directories
| Type | Convention | Example |
| --------------- | --------------------------- | --------------------------- |
| React Component | PascalCase | `UserProfile.tsx` |
| Hook | camelCase with `use` prefix | `useProject.ts` |
| Utility | kebab-case | `date-utils.ts` |
| Type file | kebab-case or `types.ts` | `types.ts`, `user-types.ts` |
| Test file | Same name + `.test` | `date-utils.test.ts` |
| Directory | kebab-case | `user-profile/` |
### Variables and Functions
| Type | Convention | Example |
| -------------- | ------------------------------------------- | ---------------------------------- |
| Variable | camelCase | `userName`, `isActive` |
| Constant | SCREAMING_SNAKE_CASE | `MAX_RETRY_COUNT` |
| Function | camelCase | `getUserById` |
| Class | PascalCase | `UserService` |
| Type/Interface | PascalCase | `UserInput`, `ProjectOutput` |
| Enum | PascalCase (type), SCREAMING_SNAKE (values) | `enum Status { ACTIVE, INACTIVE }` |
### Boolean Variables
Use `is`, `has`, `should`, `can` prefixes:
```typescript
// GOOD
const isLoading = true;
const hasPermission = user.role === 'admin';
const shouldRefresh = Date.now() > expiresAt;
const canEdit = isOwner || hasPermission;
// BAD
const loading = true;
const permission = user.role === 'admin';
```
---
## Error Handling
### Never Swallow Errors
```typescript
// BAD - Silent failure
try {
await dangerousOperation();
} catch (e) {
// nothing
}
// GOOD - Log and handle
try {
await dangerousOperation();
} catch (error) {
logger.error('operation_failed', { error });
throw new AppError('Operation failed', 'OPERATION_FAILED');
}
```
### Consistent Error Response Format
All API responses must use the standard `success` + `reason` format:
```typescript
// Success
return {
success: true,
reason: 'Operation completed successfully',
data: result,
};
// Error
return {
success: false,
reason: 'Insufficient permissions to perform this action',
};
```
---
## Dead Code Elimination
- Remove unused imports (Biome enforces this automatically)
- Remove commented-out code blocks
- Remove unused variables, functions, and types
- Remove unreachable code after `return`, `throw`, `break`, `continue`
```typescript
// BAD - Dead code
function processOrder(order: Order) {
// const oldLogic = order.items.map(...);
const result = newLogic(order);
return result;
cleanup(); // unreachable
}
// GOOD - Clean
function processOrder(order: Order) {
return newLogic(order);
}
```
---
## Lint and Type Check Before Commit
```bash
# MUST pass before every commit
pnpm lint
pnpm type-check
# Production build check (catches additional issues)
pnpm build
# Or combined
pnpm lint && pnpm type-check && pnpm build
```
---
## Testing Guidelines
### Test File Location
```
src/
__tests__/ # Integration tests
api.test.ts
app/
feature/
page.tsx
page.test.tsx # Co-located test (when appropriate)
```
### Test Structure (AAA Pattern)
```typescript
describe('OrderService', () => {
describe('createOrder', () => {
it('should create an order with valid input', async () => {
// Arrange
const input = { items: [{ productId: '1', quantity: 2 }] };
// Act
const result = await createOrder(input);
// Assert
expect(result.success).toBe(true);
expect(result.order.items).toHaveLength(1);
});
it('should reject empty order', async () => {
// Arrange
const input = { items: [] };
// Act
const result = await createOrder(input);
// Assert
expect(result.success).toBe(false);
});
});
});
```
---
## Summary
| Rule | Reason |
| ------------------------------ | ------------------- |
| No `!` assertions | Runtime errors |
| No `any` type | Type safety |
| No `@ts-expect-error` | Masks real issues |
| No `console.log` | Use structured logs |
| Lint + typecheck before commit | Consistent code |
| Structured errors | Consistent handling |
| Never swallow errors | Debuggability |
| Remove dead code | Maintainability |

View File

@@ -0,0 +1,173 @@
# Dependencies & Versions
> Adjust versions to your project. These represent a known-working combination as of the time of writing. Pin or widen ranges to match your stability requirements.
---
## Runtime Environment
| Dependency | Version | Description |
|------------|---------|-------------|
| Node.js | >=20 | JavaScript runtime |
| pnpm | ^10.x | Package manager |
---
## Core Framework
| Package | Version | Description |
|---------|---------|-------------|
| next | ^15.x | React framework for production |
| react | ^19.x | UI library |
| react-dom | ^19.x | React DOM renderer |
| typescript | ^5.x | TypeScript language |
---
## Backend
### API Layer
| Package | Version | Description |
|---------|---------|-------------|
| hono | ^4.x | Lightweight web framework |
| @orpc/server | ^1.x | oRPC server implementation |
| @orpc/client | ^1.x | oRPC client |
| @orpc/zod | ^1.x | oRPC Zod integration |
| @orpc/openapi | ^1.x | OpenAPI schema generation |
| zod | ^4.x | Schema validation |
### Database
| Package | Version | Description |
|---------|---------|-------------|
| drizzle-orm | ^0.44.x | TypeScript ORM |
| drizzle-kit | ^0.31.x | Drizzle CLI tools |
| drizzle-zod | ^0.8.x | Drizzle + Zod integration |
| pg | ^8.x | PostgreSQL client |
### Authentication
| Package | Version | Description |
|---------|---------|-------------|
| better-auth | ^1.x | Authentication library |
### Caching & Queue
| Package | Version | Description |
|---------|---------|-------------|
| @upstash/redis | ^1.x | Redis client (Upstash) |
| @upstash/qstash | ^2.x | Message queue |
---
## AI Integration
| Package | Version | Description |
|---------|---------|-------------|
| ai | ^5.x | Vercel AI SDK core |
| @ai-sdk/react | ^2.x | AI SDK React hooks |
| @ai-sdk/openai | ^2.x | OpenAI provider |
| @ai-sdk/anthropic | ^2.x | Anthropic provider |
---
## Frontend
### UI Components
| Package | Version | Description |
|---------|---------|-------------|
| @radix-ui/* | latest | Headless UI primitives |
| lucide-react | ^0.x | Icon library |
| cmdk | ^1.x | Command palette |
| sonner | ^2.x | Toast notifications |
### Styling
| Package | Version | Description |
|---------|---------|-------------|
| tailwindcss | ^4.x | Utility-first CSS (v4 config format) |
| @tailwindcss/postcss | ^4.x | PostCSS plugin |
| tailwind-merge | ^3.x | Tailwind class merging |
| class-variance-authority | ^0.7.x | Variant management |
| clsx | ^2.x | Class name utility |
### State Management
| Package | Version | Description |
|---------|---------|-------------|
| @tanstack/react-query | ^5.x | Data fetching & caching |
| @orpc/tanstack-query | ^1.x | oRPC + React Query bridge |
| nuqs | ^2.x | URL state management |
| react-hook-form | ^7.x | Form state management |
| @hookform/resolvers | ^5.x | Form validation resolvers |
### Internationalization
| Package | Version | Description |
|---------|---------|-------------|
| next-intl | ^4.x | Next.js i18n |
### Utilities
| Package | Version | Description |
|---------|---------|-------------|
| date-fns | ^4.x | Date utilities |
| es-toolkit | ^1.x | Utility functions |
| nanoid | ^5.x | ID generation |
| p-limit | ^7.x | Concurrency control |
---
## Monitoring & Logging
| Package | Version | Description |
|---------|---------|-------------|
| @sentry/nextjs | ^10.x | Error tracking |
---
## Development Tools
### Build & Bundling
| Package | Version | Description |
|---------|---------|-------------|
| turbo | ^2.x | Monorepo build system |
| tsx | ^4.x | TypeScript executor |
### Code Quality
| Package | Version | Description |
|---------|---------|-------------|
| @biomejs/biome | ^2.x | Linter & formatter |
| husky | ^9.x | Git hooks |
### Testing
| Package | Version | Description |
|---------|---------|-------------|
| @playwright/test | ^1.x | E2E testing |
---
## Important Notes
1. **React 19**: Major version with breaking changes from React 18
2. **Next.js 15**: App Router is the primary routing pattern
3. **TailwindCSS 4**: Uses the new v4 configuration format (not `tailwind.config.js`)
4. **Zod 4**: Latest version with improved TypeScript support
5. **Monorepo**: Use `@your-app/*` for internal workspace package references
---
## Updating Dependencies
When updating dependencies:
1. Check compatibility with React 19 and Next.js 15
2. Update pnpm overrides if changing React or Drizzle versions
3. Run `pnpm install` from the root directory
4. Run `pnpm type-check` to verify TypeScript compatibility
5. Run `pnpm build` to ensure production build works

View File

@@ -0,0 +1,66 @@
# Shared Development Guidelines
> These guidelines apply to all Next.js full-stack applications using this architecture.
---
## Documentation Files
| File | Description | When to Read |
| -------------------------------------- | ------------------------------------ | ----------------------- |
| [code-quality.md](./code-quality.md) | Code quality mandatory rules | Always |
| [typescript.md](./typescript.md) | TypeScript best practices | Type-related decisions |
| [dependencies.md](./dependencies.md) | Dependency versions and constraints | Adding/updating deps |
---
## Quick Navigation
| Task | File |
| --------------------------- | -------------------------------------- |
| Code quality rules | [code-quality.md](./code-quality.md) |
| Type annotations | [typescript.md](./typescript.md) |
| Dependency management | [dependencies.md](./dependencies.md) |
---
## Core Rules (MANDATORY)
| Rule | File |
| ----------------------------------------- | -------------------------------------- |
| No non-null assertions (`!`) | [code-quality.md](./code-quality.md) |
| No `any` type | [code-quality.md](./code-quality.md) |
| No `@ts-expect-error` / `@ts-ignore` | [code-quality.md](./code-quality.md) |
| No `console.log` (use structured logging) | [code-quality.md](./code-quality.md) |
| Zod-first type definitions | [typescript.md](./typescript.md) |
| Import types from backend, never redefine | [typescript.md](./typescript.md) |
| Standard response format (`success` + `reason`) | [typescript.md](./typescript.md) |
---
## Before Every Commit
- [ ] `pnpm lint` - 0 errors
- [ ] `pnpm type-check` - 0 errors
- [ ] `pnpm build` - production build succeeds
- [ ] No `any` types in new code
- [ ] No non-null assertions (`!`)
- [ ] No `@ts-expect-error` or `@ts-ignore` comments
- [ ] No `console.log` statements (use `logger`)
- [ ] Tests pass (if applicable)
---
## Code Review Checklist
- [ ] Types are explicit, not `any`
- [ ] API inputs/outputs have Zod schemas
- [ ] Error handling returns structured responses
- [ ] No duplicate type definitions (import from source of truth)
- [ ] Naming follows conventions (files: kebab-case, components: PascalCase)
- [ ] Unused imports and dead code removed
- [ ] No swallowed errors (silent `catch` blocks)
---
**Language**: All documentation must be written in **English**.

View File

@@ -0,0 +1,441 @@
# TypeScript Best Practices
> TypeScript guidelines for Next.js full-stack applications.
---
## Zod-First Type Definitions
Define Zod schemas first, then infer TypeScript types from them. Never define types manually when a Zod schema exists.
```typescript
import { z } from 'zod';
// 1. Define the schema (single source of truth)
export const createUserInputSchema = z.object({
name: z.string().min(1).max(100),
email: z.string().email(),
role: z.enum(['admin', 'member', 'viewer']),
});
export const createUserOutputSchema = z.object({
success: z.boolean(),
reason: z.string(),
user: z.object({
id: z.string(),
name: z.string(),
email: z.string(),
}).optional(),
});
// 2. Derive types from schemas
export type CreateUserInput = z.infer<typeof createUserInputSchema>;
export type CreateUserOutput = z.infer<typeof createUserOutputSchema>;
// BAD - Manual type that duplicates schema
interface CreateUserInput {
name: string;
email: string;
role: 'admin' | 'member' | 'viewer';
}
```
### Reusable Base Schemas
```typescript
const paginationSchema = z.object({
page: z.number().min(1).default(1),
limit: z.number().min(1).max(100).default(20),
});
const timestampSchema = z.object({
createdAt: z.string().datetime(),
updatedAt: z.string().datetime(),
});
// Compose into larger schemas
export const listOrdersInputSchema = paginationSchema.extend({
status: orderStatusZodSchema.optional(),
customerId: z.string().optional(),
});
```
---
## Type Inference from API
Import types from the backend or infer them from the API client. Never redefine backend types on the frontend.
### Import from Backend Package
```typescript
// GOOD - Import from the API package
import type { User, Order } from '@your-app/api/modules/users/types';
import type { OrderStatus } from '@your-app/api/modules/orders/types';
// BAD - Redefining types that exist in backend
interface User {
id: string;
name: string;
email: string;
}
```
### Infer from API Client
```typescript
import { orpcClient } from '@/lib/orpc';
// Infer the response type from the API client
type UsersResponse = Awaited<ReturnType<typeof orpcClient.users.list>>;
// Infer a single item type from array response
type User = UsersResponse['items'][number];
// Infer input types
type CreateUserInput = Parameters<typeof orpcClient.users.create>[0];
```
### Type Inference in Hooks
```typescript
// The return type is automatically inferred from oRPC
export function useUsers() {
return useQuery({
queryKey: ['users'],
queryFn: () => orpcClient.users.list(),
});
}
// For complex transformations, use explicit inference
type UserListData = Awaited<ReturnType<typeof orpcClient.users.list>>;
export function useFormattedUsers() {
return useQuery({
queryKey: ['users', 'formatted'],
queryFn: async () => {
const data = await orpcClient.users.list();
return transformUsers(data);
},
});
}
```
---
## Discriminated Unions
Use discriminated unions for types that can be one of several shapes. Use strict equality (`=== true`) for narrowing.
### TypeScript Discriminated Union
```typescript
type Result<T> =
| { success: true; data: T }
| { success: false; error: string };
const result: Result<User> = doSomething();
// CORRECT: Use === true for narrowing
if (result.success === true) {
console.log(result.data); // TypeScript knows data exists
} else {
console.log(result.error); // TypeScript knows error exists
}
```
### Zod Discriminated Union
```typescript
export const notificationSchema = z.discriminatedUnion('type', [
z.object({
type: z.literal('email'),
recipient: z.string().email(),
subject: z.string(),
}),
z.object({
type: z.literal('sms'),
phoneNumber: z.string(),
message: z.string(),
}),
z.object({
type: z.literal('push'),
deviceToken: z.string(),
title: z.string(),
body: z.string(),
}),
]);
type Notification = z.infer<typeof notificationSchema>;
```
---
## Generic Patterns
### Generic Result Type
```typescript
type Result<T> =
| { success: true; data: T }
| { success: false; error: string };
function createResult<T>(data: T): Result<T> {
return { success: true, data };
}
```
### Generic Paginated Response
```typescript
type PaginatedResponse<T> = {
items: T[];
total: number;
page: number;
pageSize: number;
};
type UserListResponse = PaginatedResponse<User>;
```
### Generic with Constraints
```typescript
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
```
### Common Utility Types
```typescript
// Extract array element type
type ArrayElement<T> = T extends (infer E)[] ? E : never;
// Make specific properties optional
type PartialBy<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
// Make specific properties required
type RequiredBy<T, K extends keyof T> = Omit<T, K> & Required<Pick<T, K>>;
```
---
## Standard Response Format
All API responses must include `success` and `reason` fields.
```typescript
// Output schema pattern
export const operationResultSchema = z.object({
success: z.boolean(),
reason: z.string(),
data: z.unknown().optional(),
});
// Success response
return {
success: true,
reason: 'User created successfully',
user: { id, name, email },
};
// Error response
return {
success: false,
reason: 'Email address is already in use',
};
```
### Batch Operation Response
```typescript
export const batchOperationResultSchema = z.object({
success: z.boolean(),
total: z.number(),
processed: z.number(),
failed: z.number(),
errors: z.array(z.object({
itemId: z.string(),
error: z.string(),
})).optional(),
});
```
---
## Forbidden Patterns
### No `any`
```typescript
// BAD
function process(data: any) { ... }
// GOOD
function process(data: unknown) { ... }
function process(data: ProcessInput) { ... }
```
### No Non-null Assertion
```typescript
// BAD
const name = user!.name;
const first = items[0]!;
// GOOD
if (user) {
const name = user.name;
}
const first = items[0];
if (!first) {
return { success: false, reason: 'No items found' };
}
```
### No `@ts-expect-error` / `@ts-ignore`
```typescript
// BAD
// @ts-expect-error - customField exists at runtime
const value = user.customField;
// GOOD - Update the type definition instead
interface User {
customField: string;
// ...
}
```
### No Type Assertions Without Validation
```typescript
// BAD - Blind assertion
const user = data as User;
// GOOD - Runtime validation with Zod
const user = userSchema.parse(data);
// GOOD - Type guard
function isUser(data: unknown): data is User {
return (
typeof data === 'object' &&
data !== null &&
'id' in data &&
'email' in data
);
}
```
---
## Type Imports
Always use `import type` for type-only imports:
```typescript
// GOOD
import type { User, Project } from './types';
import { createUser } from './procedures';
// Also acceptable
import { type User, createUser } from './types';
// BAD
import { User, createUser } from './types';
```
---
## Explicit Return Types for Exports
Always annotate explicit return types on exported functions:
```typescript
// BAD - Implicit return type
export function getUser(id: string) {
return db.query.users.findFirst({ where: eq(users.id, id) });
}
// GOOD - Explicit return type
export function getUser(id: string): Promise<User | undefined> {
return db.query.users.findFirst({ where: eq(users.id, id) });
}
```
---
## TypeScript Configuration
Ensure strict mode is enabled:
```json
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"noImplicitReturns": true,
"noUncheckedIndexedAccess": true
}
}
```
---
## Drizzle Type Inference
```typescript
// Infer types from Drizzle tables
type User = typeof userTable.$inferSelect;
type NewUser = typeof userTable.$inferInsert;
// Combine with Zod via drizzle-zod
import { createSelectSchema, createInsertSchema } from 'drizzle-zod';
const userSelectSchema = createSelectSchema(userTable);
const userInsertSchema = createInsertSchema(userTable);
```
---
## View Model Types
When the frontend needs computed properties, extend backend types rather than redefining them:
```typescript
import type { Order } from '@your-app/api/modules/orders/types';
export interface OrderViewModel extends Order {
formattedTotal: string;
statusLabel: string;
isEditable: boolean;
}
export function toOrderViewModel(order: Order): OrderViewModel {
return {
...order,
formattedTotal: formatCurrency(order.total),
statusLabel: getStatusLabel(order.status),
isEditable: order.status === 'draft',
};
}
```
---
## Summary
| Practice | Reason |
| --------------------------- | ----------------------------- |
| Zod-first types | Single source of truth |
| Import, don't redefine | No type drift |
| `=== true` for unions | Proper narrowing |
| Generics for reuse | DRY, type-safe |
| `success` + `reason` format | Consistent API responses |
| No `any` | Type safety |
| No `!` assertions | Runtime safety |
| No `@ts-expect-error` | Masks real issues |
| `import type` | Clear separation, tree-shake |
| Explicit return types | Documentation, catch errors |