--- name: project-guidelines description: Guide for working with Nimara's layered monorepo architecture. Use when adding new features, choosing which package to use, understanding data flow, or making architectural decisions. Covers domain/infrastructure/foundation/features layers and their responsibilities. --- # Nimara Project Guidelines & Architecture This skill helps you navigate Nimara's **layered monorepo architecture** and make correct decisions about where code belongs and how layers interact. ## Quick Layer Reference ``` apps/ # User-facing applications ├── storefront/ # Next.js customer-facing storefront ├── marketplace/ # Vendor dashboard, ledger, payouts ├── stripe/ # Stripe payment integration app └── automated-tests/ # CodeceptJS E2E tests packages/ # Shared, reusable code ├── domain/ # Pure business logic (no framework deps) ├── infrastructure/ # External integrations (APIs, GraphQL) ├── foundation/ # Utilities, hooks, helpers ├── features/ # Feature implementations (UI + logic) ├── ui/ # Shared UI components (Shadcn-style) └── config/ # Shared configs (Tailwind, ESLint, etc.) ``` --- ## Package Layers Explained ### 1. Domain Layer (`@nimara/domain`) **Responsibility:** Pure business logic, types, constants, entities—zero external dependencies. **What belongs here:** - TypeScript types and interfaces (Product, Order, User, etc.) - Enums and constants (PaymentStatus, OrderStatus, etc.) - DTOs (Data Transfer Objects) for serialization - Pure utility functions with no side effects - Error types and Result pattern - Business rules and validators **What does NOT belong here:** - Framework imports (React, Next.js, Node APIs) - External API clients (Saleor, ButterCMS) - Database operations - HTTP requests - Environment variables **Example structure:** ``` packages/domain/ ├── objects/ │ ├── Result.ts # Result type │ └── Product.ts # Product entity ├── types/ │ ├── Cart.types.ts │ └── Order.types.ts ├── constants/ │ ├── paymentStatus.ts │ └── orderStatus.ts └── validators/ └── emailValidator.ts ``` **Key rule:** Domain is a **leaf package**—it imports nothing from other Nimara packages. --- ### 2. Foundation Layer (`@nimara/foundation`) **Responsibility:** Core utilities, React hooks, helpers—framework-aware but integration-agnostic. **What belongs here:** - React hooks (useLocalStorage, usePagination, etc.) - Utility functions (formatPrice, formatDate, slugify, etc.) - Constants and helpers used across apps - Custom error classes - Shared adapters for third-party libraries - Shared type utilities and helpers **What does NOT belong here:** - Feature-specific logic (belongs in `features` or `infrastructure`) - API clients or external integrations - UI components (those go in `ui`) - App-specific configurations **Example structure:** ``` packages/foundation/ ├── hooks/ │ ├── usePagination.ts │ └── useLocalStorage.ts ├── utils/ │ ├── formatPrice.ts │ ├── formatDate.ts │ └── slugify.ts └── types/ └── common.types.ts ``` **Key rule:** Foundation is a **leaf package**—it depends only on `domain`. --- ### 3. Infrastructure Layer (`@nimara/infrastructure`) **Responsibility:** External integrations (Saleor, ButterCMS, Algolia, Stripe, etc.) and GraphQL operations. **What belongs here:** - GraphQL queries and mutations (.graphql files + generated types) - External API clients and service adapters - Data providers (Saleor product provider, CMS provider, etc.) - Serializers and transformers (raw API response → domain types) - Use-cases that coordinate infrastructure calls - Result-based error handling for external operations **What does NOT belong here:** - React components - UI logic - App-specific routing - Server Actions (those live in apps) **Example structure:** ``` packages/infrastructure/ ├── saleor/ │ ├── queries/ │ │ ├── GetProducts.graphql │ │ └── GetProducts.generated.ts │ ├── mutations/ │ │ └── AddToCart.graphql │ ├── client.ts # Saleor client config │ └── providers/ │ └── ProductProvider.ts ├── butter-cms/ │ ├── client.ts │ └── providers/ │ └── PageProvider.ts ├── algolia/ │ ├── client.ts │ └── search.ts └── use-cases/ ├── getProduct.ts ├── searchProducts.ts └── addToCart.ts ``` **Key rule:** Infrastructure depends on `domain` + `foundation`. Services in infrastructure should return `Result` for operations that can fail. --- ### 4. Features Layer (`@nimara/features`) **Responsibility:** Feature implementations—combining UI, state, and business logic into cohesive features. **What belongs here:** - Feature-specific React components - Feature state management (Context, hooks) - Feature-specific Server Actions - Feature forms and validation - Feature-specific utilities - Feature documentation **What does NOT belong here:** - Shared UI primitives (those go in `ui`) - Core utilities (those go in `foundation`) - Business logic (that's `domain`) **Example structure:** ``` packages/features/ ├── checkout/ │ ├── Checkout.tsx │ ├── CheckoutProvider.tsx │ ├── useCheckout.ts │ ├── checkoutFormSchema.ts │ └── _actions/ │ └── completeCheckout.ts ├── product-search/ │ ├── ProductSearch.tsx │ ├── SearchFilters.tsx │ └── useProductSearch.ts └── user-account/ ├── AccountSettings.tsx ├── useUserProfile.ts └── _actions/ └── updateProfile.ts ``` **Key rule:** Features depends on all packages (`domain`, `foundation`, `infrastructure`, `ui`). This is the **only** layer that depends on everything. --- ### 5. UI Layer (`@nimara/ui`) **Responsibility:** Reusable UI components and design system—presentational, not business logic. **What belongs here:** - UI primitives (Button, Input, Card, Modal, etc.) - Form components - Layout components - Icons and SVGs - Design tokens and theme utilities - Shared styles and Tailwind extensions **What does NOT belong here:** - Business logic - Feature-specific components - State management - Server Actions **Example structure:** ``` packages/ui/ ├── components/ │ ├── Button.tsx │ ├── Input.tsx │ ├── Modal.tsx │ ├── Card.tsx │ └── Form/ │ ├── FormField.tsx │ └── FormError.tsx ├── icons/ │ ├── ChevronDown.tsx │ └── ShoppingCart.tsx ├── layouts/ │ ├── Container.tsx │ └── Grid.tsx └── theme/ └── colors.ts ``` **Key rule:** UI is a **leaf package** or minimal-dependency—imports only `domain` and `foundation` if needed. --- ## Dependency Flow (CRITICAL) The dependency direction is **unidirectional**: ``` domain (leaf) ↑ foundation (leaf) ↑ infrastructure ─┐ ↑ │ └─ domain │ └─ foundation│ ├→ features ui (leaf) │ ↑ │ ui ─────────────┘ ↑ └─ domain, foundation apps (storefront, stripe) ↑ └─ features, infrastructure, ui, foundation, domain ``` **In practice:** - Apps import from: `features`, `infrastructure`, `ui`, `foundation`, `domain` - Features import from: `infrastructure`, `ui`, `foundation`, `domain` (all packages!) - Infrastructure imports from: `domain`, `foundation` - Foundation imports from: `domain` only - Domain imports from: nothing (pure business logic) - UI imports from: `domain`, `foundation` (minimal) **What NEVER happens:** - ❌ Domain imports anything else - ❌ Infra imports from Features, UI, or apps - ❌ Foundation imports from Infra, Features, or UI - ❌ Apps import from other apps --- ## Decision Tree: Where Does This Code Belong? ### Question 1: Is it pure business logic (no framework, no API calls)? - **Yes → `domain`** - Type definitions, constants, validators - **No → Next question** ### Question 2: Is it a utility/helper function or React hook? - **Yes → `foundation`** - Helper functions, hooks, adapters - **No → Next question** ### Question 3: Is it an external API integration or GraphQL operation? - **Yes → `infrastructure`** - API clients, GraphQL, data providers - **No → Next question** ### Question 4: Is it a reusable UI component (no business logic)? - **Yes → `ui`** - Buttons, inputs, modals, layouts - **No → Next question** ### Question 5: Is it a feature combining UI + logic? - **Yes → `features`** - Checkout form, product search, account settings - **No → It's app-specific → Put in `apps/storefront/` or `apps/stripe/`** --- ## Common Scenarios ### Scenario 1: Add a new payment method integration 1. **Domain:** Add payment type/status constants ```typescript // packages/domain/constants/paymentStatus.ts export const PAYMENT_METHODS = { CREDIT_CARD: "credit_card", DIGITAL_WALLET: "digital_wallet", } as const; ``` 2. **Infrastructure:** Create the payment provider + GraphQL mutations ``` packages/infrastructure/payments/ ├── mutations/CreatePayment.graphql ├── mutations/CreatePayment.generated.ts └── providers/PaymentProvider.ts ``` 3. **Features:** Create the payment form component ``` packages/features/payment-form/ ├── PaymentForm.tsx └── usePayment.ts ``` 4. **App:** Use in checkout page ```typescript // apps/storefront/src/app/[locale]/(checkout)/payment/page.tsx import { PaymentForm } from '@nimara/features'; export default function PaymentPage() { return ; } ``` ### Scenario 2: Add a utility function 1. **Check if it's domain logic (pure business rules):** If yes → `domain` 2. **Check if it's a helper used across features:** If yes → `foundation` 3. **If feature-specific:** Keep it in the feature folder --- ## Dependency Management (CRITICAL) **RULE: NEVER automatically install new dependencies. ALWAYS require explicit user approval first.** ### Why This Matters - Dependencies affect bundle size, security, and maintenance burden - Each package adds complexity and potential conflicts - Alternatives exist and should be evaluated first - Team should agree on tech choices ### Approval Workflow 1. **Identify the need** — You need a library for a feature 2. **Ask for approval** — List the dependency and explain why 3. **Provide alternatives** — Show other options the user could consider 4. **Wait for confirmation** — User must explicitly approve 5. **Install after approval** — Only then run `pnpm add` or `pnpm add -D` ### Template for Asking Approval ``` I need to add a new dependency to [package-name]: Package: library-name Version: X.Y.Z Purpose: Brief description of what it's used for Reasons to choose this: - Reason 1 - Reason 2 Alternatives considered: - Alternative 1 (pros/cons) - Alternative 2 (pros/cons) Where it will be used: - packages/infrastructure/... - apps/storefront/... Should I proceed with installation? ``` ### Examples **❌ WRONG: Automatically installing** ```bash pnpm add lodash-es # Package added without asking! ``` **✅ RIGHT: Asking for approval first** ``` I need to add `zod` to `@nimara/domain` for schema validation. Reasons: - Runtime validation with TypeScript integration - Already used in the project - Lightweight (~8kb gzipped) Alternatives: - io-ts (more complex, better for complex validations) - joi (heavier, more features) Should I add zod to packages/domain/package.json? ``` ### When You DO Have Approval Once the user approves, proceed: ```bash # Navigate to the correct package cd packages/domain # Install the dependency pnpm add zod # Or dev dependency pnpm add -D vitest # Verify it was added to package.json git diff package.json ``` ### Common Dependency Scenarios #### Scenario 1: Adding a UI component library **Ask for approval because:** - Affects bundle size significantly - May introduce styling conflicts - Should be evaluated for design system alignment **Before adding:** ``` Should I add shadcn-ui@next to packages/ui? - Already used in the project - Gives access to pre-built accessible components - Can be customized with Tailwind Approve? (yes/no) ``` #### Scenario 2: Adding a validation library **Ask for approval because:** - Core architectural decision - Affects how data is validated across the app - Should align with existing patterns **Before adding:** ``` Should I add zod to @nimara/domain? - Runtime validation with TS types - Lightweight and performant - Already partially used in infrastructure Approve? (yes/no) ``` #### Scenario 3: Adding a utility library **Ask for approval because:** - Can often be replaced with native JS/TS - Adds to dependency tree - May have better alternatives **Before adding:** ``` Should I add date-fns to @nimara/foundation? - For date formatting and manipulation - More tree-shakeable than moment.js - Smaller bundle than other alternatives Approve? (yes/no) ``` ### Exception: Dependencies Already in Project If a dependency is **already installed and used** elsewhere in the project, you don't need approval to use it in a new package—but you still cannot install it without asking. ``` ✅ OK to use: "I'll use the existing zod dependency" ❌ NOT OK: "I'll add zod" (without asking) ``` ### Verifying Installation After installing, always verify: ```bash # Check package.json was updated cat packages/your-package/package.json | grep "dependency-name" # Verify pnpm.lock was updated git diff pnpm.lock | head -20 # Ensure it's in the right place (dependencies vs devDependencies) ``` ### Adding to Package.json Directly If you're editing `package.json` manually (not recommended), still ask first: ``` I need to add "zod": "^3.22.0" to packages/domain/package.json Should I proceed? (yes/no) ``` ### Monorepo Dependency Rules **Root workspace:** - Only devDependencies (turbo, prettier, eslint, etc.) - Version management tools **App/package package.json:** - Only dependencies that app/package actually uses - No "just in case" dependencies **Shared packages:** - Careful about adding to `@nimara/domain` (it should be minimal) - Infrastructure can have more dependencies (it talks to APIs) - Foundation utilities should be lightweight --- ## Architecture Best Practices ### 1. Keep Domain Pure - Domain should be the most stable, reusable layer - No framework, no external dependencies - Easy to test, easy to share ### 2. Use Result Pattern in Infrastructure ```typescript // packages/infrastructure/saleor/products/getProduct.ts import { Result } from "@nimara/domain/objects/Result"; export async function getProduct(id: string): Promise> { try { const product = await saleorClient.getProduct(id); return { ok: true, data: product }; } catch (error) { return { ok: false, error }; } } ``` ### 3. Consume Infrastructure in Features ```typescript // packages/features/product-detail/useProduct.ts import { getProduct } from "@nimara/infrastructure/saleor/products"; export function useProduct(id: string) { const [result, setResult] = useState>({ ok: false }); // ... useEffect(() => { getProduct(id).then(setResult); }, [id]); return result; } ``` ### 4. Server Actions Belong in App or Features ```typescript // packages/features/checkout/_actions/completeCheckout.ts "use server"; import { completeCheckoutUseCase } from "@nimara/infrastructure"; export async function completeCheckout(data: CheckoutData) { const result = await completeCheckoutUseCase(data); if (!result.ok) return { error: result.error }; revalidatePath("/orders"); return { success: true }; } ``` ### 5. Avoid Circular Dependencies - Feature A imports from Infrastructure ✅ - Infrastructure should NOT import from Feature A ❌ - If you need circular data flow, use Context or lift state ### 6. Options Object Over Positional Params A function/factory taking more than one argument accepts a single options object, not positional params — more readable at the call site and easy to extend without breaking callers. ```typescript // ❌ positional export const clientEntryPoint = (context: Context, appName: string) => { … }; clientEntryPoint(context, "handler"); // ✅ options object export const clientEntryPoint = ({ appName, context, }: { appName: string; context: Context; }) => { … }; clientEntryPoint({ context, appName: "handler" }); ``` A single argument stays positional (`getOperationName(document)`). ### 7. Use-cases Return a Callable A single-operation use-case returns the callable directly — no `{ execute }` wrapper (that's ceremony). Reserve method objects for genuine multi-operation **services** (`joseAuthService`, `appConfigService`). ```typescript // ✅ callable use-case export const installSaleorAppUseCase = (deps): InstallSaleorAppUseCase => async (input) => { … }; container.get("installAppUseCase")(input); ``` --- ## Common Gotchas ### Gotcha 1: Putting API client logic in `domain` ```typescript // ❌ WRONG: domain should not have external deps // packages/domain/services/ProductService.ts import axios from "axios"; export function getProduct(id: string) { return axios.get(`/api/products/${id}`); } // ✅ RIGHT: move to infrastructure // packages/infrastructure/saleor/products/getProduct.ts ``` ### Gotcha 2: Importing Features from Infrastructure ```typescript // ❌ WRONG: Creates circular dependency // packages/infrastructure/use-cases/checkout.ts import { CheckoutForm } from "@nimara/features"; // ✅ RIGHT: Infrastructure provides data, features consume it ``` ### Gotcha 3: App-specific code in shared packages ```typescript // ❌ WRONG: This is storefront-specific // packages/features/StorefrontHeader.tsx // ✅ RIGHT: Keep in app // apps/storefront/src/components/StorefrontHeader.tsx ``` ### Gotcha 4: UI components with business logic ```typescript // ❌ WRONG: Business logic in UI // packages/ui/ProductCard.tsx const ProductCard = ({ productId }) => { const product = await fetchProduct(productId); // ❌ return
{product.name}
; }; // ✅ RIGHT: UI receives data as prop // packages/ui/ProductCard.tsx const ProductCard = ({ product }: { product: Product }) => { return
{product.name}
; }; ``` --- ## When Adding a New Feature Follow this checklist: 1. **Create domain types** (if new entity type) - `packages/domain/types/YourEntity.types.ts` - Add to domain constants if needed 2. **Add infrastructure integration** (if external API call) - `packages/infrastructure/your-provider/` - Create GraphQL queries if Saleor - Implement Result-based error handling 3. **Create feature package** - `packages/features/your-feature/` - Implement components and hooks - Add Server Actions if mutations 4. **Use in app** - Import from `@nimara/features` - Wire up in routing/layouts 5. **Add tests** - Unit tests in each package - E2E tests in `apps/automated-tests` --- ## Resources - **Global rules and monorepo layout:** See the root `CLAUDE.md` - **Layer and dependency rules:** See `.claude/rules/architecture.md` - **Architecture examples:** See `references/architecture-examples.md` - **Dependency procedures:** See `references/dependency-management.md`