Development
Conventions
| Workspace | Convention | Example |
|---|
| be-packages | feature-<domain> | feature-farmer-registry |
| fe-packages | feature-<domain>-web | feature-farmer-registry-web |
| shared contracts | <domain>-contracts | eudr-compliance-contracts |
| shared utilities | descriptive noun | auth-permissions |
Always import from the package barrel. Never use deep imports.
typescript
// ✅ correct — barrel import
import { EnrollmentWizard, useFarmers } from '@repo/feature-farmer-registry-web';
// ❌ wrong — deep import breaks the public API contract
import { EnrollmentWizard } from '@repo/feature-farmer-registry-web/src/components/EnrollmentWizard';
Cross-workspace imports are forbidden. apps/web cannot import from be-packages; fe-packages cannot import from be-packages. TypeScript path aliases enforce this — npm run typecheck will fail if violated.
| Rule | Detail |
|---|
| Strict mode | All workspaces extend @repo/typescript-config. strictNullChecks, noImplicitAny, etc. are on. |
| No any | Use unknown for unknown shapes. Use type guards to narrow. No any except at system boundaries with comment justification. |
| Discriminated unions | Preferred for domain concepts with multiple shapes (e.g. risk level, farmer status). |
| Branded types | Use for IDs and domain scalars to prevent accidental mixing. |
| Deep conditional types | Avoid unless the simpler shape is genuinely insufficient. |
Define Zod schemas in be-packages/feature-*/src/schema/ or shared/validation/. Infer TypeScript types from those schemas using z.infer. Never write a TypeScript type that duplicates a Zod schema shape.
typescript
// ✅ correct — type inferred from schema
const CreateFarmerSchema = z.object({
firstName: z.string().min(1),
nationalId: z.string().min(6),
communityId: z.string().uuid(),
});
type CreateFarmerInput = z.infer<typeof CreateFarmerSchema>;
// ❌ wrong — manual type duplicates the schema
interface CreateFarmerInput {
firstName: string;
nationalId: string;
communityId: string;
}
| Rule | Detail |
|---|
| Function components only | No class components. |
| Hooks for state | Keep components lean. Extract stateful logic into named hooks. |
| No business logic in pages | Route page files orchestrate layout and pass props. Business logic lives in feature packages. |
| Context sparingly | Prefer SWR for server state. Avoid creating new context providers for feature state. |
| No inline styles | Use Tailwind classes. Use @apply for repeated utility patterns in globals.css. |
| Requirement | Detail |
|---|
| Service unit tests | At least one Vitest unit test per new service function. |
| Route smoke tests | At least one smoke test per new API route (happy path + invalid input 422). |
| No mocked databases | Integration tests hit a real Neon branch database. Don't mock the ORM layer. |
| Test file location | Co-located with source: feature-farmer-registry/src/services/farmer.service.test.ts |
bash
# Run all tests
npm run test
# Run tests for one package
npm run test --workspace=feature-farmer-registry
# Run in watch mode
npm run test --workspace=feature-farmer-registry -- --watch
| Tool | Command | Notes |
|---|
| ESLint | npm run lint | v9 flat config. --max-warnings 0 enforced — zero warnings allowed. |
| Prettier | npm run format | Auto-formats all files. Run before committing. |
| TypeScript | npm run typecheck | Full type check across all workspaces. |
Zero warnings policyESLint runs with --max-warnings 0 in CI. Any ESLint warning is treated as an error. Fix warnings before pushing.
| Convention | Detail |
|---|
| Validation | Every route uses @hono/zod-validator. 422 Unprocessable Entity on schema failure. |
| Error shape | { error: { code: string, message: string, details?: unknown } } |
| Tenant scoping | Every query includes WHERE org_id = $orgId extracted from JWT claims. |
| HTTP methods | GET (read), POST (create), PATCH (update), DELETE (delete). No PUT. |
| Route naming | Plural nouns: /farmers, /farms, /surveys. Nested: /surveys/:id/questions |
Default to writing no comments. Only add a comment when the why is non-obvious: a hidden constraint, a subtle invariant, or a workaround for a specific bug. Well-named identifiers communicate what the code does. If removing the comment would not confuse a future reader, don't write it.