# Recommended Portal Structure: `lib/` vs `features/` ## 🎯 Core Principle: Co-location **Feature-specific code belongs WITH the feature, not in a centralized `lib/` folder.** --- ## 📂 Proposed Structure ``` apps/portal/src/ ├── lib/ # ✅ ONLY truly generic utilities │ ├── api/ │ │ ├── client.ts # API client instance & configuration │ │ ├── query-keys.ts # React Query key factory │ │ ├── helpers.ts # getDataOrThrow, getDataOrDefault │ │ └── index.ts # Barrel export │ │ │ ├── utils/ │ │ ├── cn.ts # Tailwind className utility (used everywhere) │ │ ├── error-handling.ts # Generic error handling (used everywhere) │ │ └── index.ts │ │ │ ├── providers.tsx # Root-level React context providers │ └── index.ts # Main barrel export │ └── features/ # ✅ Feature-specific code lives here ├── billing/ │ ├── components/ │ │ ├── InvoiceList.tsx │ │ └── InvoiceCard.tsx │ ├── hooks/ │ │ └── useBilling.ts # ← Feature-specific hooks HERE │ ├── utils/ │ │ └── invoice-helpers.ts # ← Feature-specific utilities HERE │ └── index.ts │ ├── subscriptions/ │ ├── components/ │ ├── hooks/ │ │ └── useSubscriptions.ts │ └── index.ts │ ├── orders/ │ ├── components/ │ ├── hooks/ │ │ └── useOrders.ts │ └── index.ts │ └── auth/ ├── components/ ├── hooks/ │ └── useAuth.ts ├── services/ │ └── auth.store.ts └── index.ts ``` --- ## 🎨 What Goes Where? ### ✅ `lib/` - Truly Generic, Reusable Across Features | File | Purpose | Used By | |------|---------|---------| | `lib/api/client.ts` | API client instance | All features | | `lib/api/query-keys.ts` | React Query keys factory | All features | | `lib/api/helpers.ts` | `getDataOrThrow`, `getDataOrDefault` | All features | | `lib/utils/cn.ts` | Tailwind className merger | All components | | `lib/utils/error-handling.ts` | Generic error parsing | All features | | `lib/providers.tsx` | Root providers (QueryClient, Theme) | App root | ### ✅ `features/*/hooks/` - Feature-Specific Hooks | File | Purpose | Used By | |------|---------|---------| | `features/billing/hooks/useBilling.ts` | Invoice queries & mutations | Billing pages only | | `features/subscriptions/hooks/useSubscriptions.ts` | Subscription queries | Subscription pages only | | `features/orders/hooks/useOrders.ts` | Order queries | Order pages only | | `features/auth/hooks/useAuth.ts` | Auth state & actions | Auth-related components | --- ## ❌ Anti-Pattern: Centralized Feature Hooks ``` lib/ ├── hooks/ │ ├── use-billing.ts # ❌ BAD - billing-specific │ ├── use-subscriptions.ts # ❌ BAD - subscriptions-specific │ └── use-orders.ts # ❌ BAD - orders-specific ``` **Why this is bad:** - Hard to find (is it in `lib` or `features`?) - Breaks feature encapsulation - Harder to delete features - Makes the `lib` folder bloated --- ## 📝 Implementation ### 1. **`lib/api/index.ts`** - Clean API Exports ```typescript /** * API Client Utilities * Central export for all API-related functionality */ export { apiClient } from "./client"; export { queryKeys } from "./query-keys"; export { getDataOrThrow, getDataOrDefault, isApiError } from "./helpers"; // Re-export common types from generated client export type { QueryParams, PathParams } from "./runtime/client"; ``` ### 2. **`lib/api/helpers.ts`** - API Helper Functions ```typescript import type { ApiResponse } from "@customer-portal/domain/common"; export function getDataOrThrow( response: { data?: T; error?: unknown }, errorMessage: string ): T { if (response.error || !response.data) { throw new Error(errorMessage); } return response.data; } export function getDataOrDefault( response: { data?: T; error?: unknown }, defaultValue: T ): T { return response.data ?? defaultValue; } export function isApiError(error: unknown): error is Error { return error instanceof Error; } ``` ### 3. **`features/billing/hooks/useBilling.ts`** - Clean Hook Implementation ```typescript "use client"; import { useQuery, useMutation } from "@tanstack/react-query"; import { apiClient, queryKeys, getDataOrThrow, getDataOrDefault } from "@/lib/api"; import type { QueryParams } from "@/lib/api"; // ✅ Single consolidated import from domain import { // Types type Invoice, type InvoiceList, type InvoiceQueryParams, type InvoiceSsoLink, type PaymentMethodList, // Schemas invoiceSchema, invoiceListSchema, // Constants INVOICE_STATUS, type InvoiceStatus, } from "@customer-portal/domain/billing"; // Constants const EMPTY_INVOICE_LIST: InvoiceList = { invoices: [], pagination: { page: 1, totalItems: 0, totalPages: 0 }, }; const EMPTY_PAYMENT_METHODS: PaymentMethodList = { paymentMethods: [], totalCount: 0, }; // Helper functions function ensureInvoiceStatus(invoice: Invoice): Invoice { return { ...invoice, status: (invoice.status as InvoiceStatus) ?? INVOICE_STATUS.DRAFT, }; } function normalizeInvoiceList(list: InvoiceList): InvoiceList { return { ...list, invoices: list.invoices.map(ensureInvoiceStatus), pagination: { page: list.pagination?.page ?? 1, totalItems: list.pagination?.totalItems ?? 0, totalPages: list.pagination?.totalPages ?? 0, nextCursor: list.pagination?.nextCursor, }, }; } function toQueryParams(params: InvoiceQueryParams): QueryParams { return Object.entries(params).reduce((acc, [key, value]) => { if (value !== undefined) { acc[key] = value; } return acc; }, {} as QueryParams); } // API functions (keep as internal implementation details) async function fetchInvoices(params?: InvoiceQueryParams): Promise { const query = params ? toQueryParams(params) : undefined; const response = await apiClient.GET( "/api/invoices", query ? { params: { query } } : undefined ); const data = getDataOrDefault(response, EMPTY_INVOICE_LIST); const parsed = invoiceListSchema.parse(data); return normalizeInvoiceList(parsed); } async function fetchInvoice(id: string): Promise { const response = await apiClient.GET("/api/invoices/{id}", { params: { path: { id } }, }); const invoice = getDataOrThrow(response, "Invoice not found"); const parsed = invoiceSchema.parse(invoice); return ensureInvoiceStatus(parsed); } async function fetchPaymentMethods(): Promise { const response = await apiClient.GET("/api/invoices/payment-methods"); return getDataOrDefault(response, EMPTY_PAYMENT_METHODS); } // Exported hooks export function useInvoices(params?: InvoiceQueryParams) { return useQuery({ queryKey: queryKeys.billing.invoices(params), queryFn: () => fetchInvoices(params), }); } export function useInvoice(id: string, enabled = true) { return useQuery({ queryKey: queryKeys.billing.invoice(id), queryFn: () => fetchInvoice(id), enabled: Boolean(id) && enabled, }); } export function usePaymentMethods() { return useQuery({ queryKey: queryKeys.billing.paymentMethods(), queryFn: fetchPaymentMethods, }); } export function useCreateInvoiceSsoLink() { return useMutation({ mutationFn: async ({ invoiceId, target }: { invoiceId: number; target?: "view" | "download" | "pay" }) => { const response = await apiClient.POST("/api/invoices/{id}/sso-link", { params: { path: { id: invoiceId }, query: target ? { target } : undefined, }, }); return getDataOrThrow(response, "Failed to create SSO link"); }, }); } export function useCreatePaymentMethodsSsoLink() { return useMutation({ mutationFn: async () => { const response = await apiClient.POST("/auth/sso-link", { body: { destination: "index.php?rp=/account/paymentmethods" }, }); return getDataOrThrow(response, "Failed to create payment methods SSO link"); }, }); } ``` ### 4. **`features/billing/index.ts`** - Feature Barrel Export ```typescript /** * Billing Feature Exports */ // Hooks export * from "./hooks/useBilling"; // Components (if you want to export them) export * from "./components/InvoiceList"; export * from "./components/InvoiceCard"; ``` ### 5. **`lib/index.ts`** - Generic Utilities Only ```typescript /** * Portal Library * ONLY generic utilities used across features */ // API utilities (used by all features) export * from "./api"; // Generic utilities (used by all features) export * from "./utils"; // NOTE: Feature-specific hooks are NOT exported here // Import them from their respective features: // import { useInvoices } from "@/features/billing"; ``` --- ## 🎯 Usage After Restructure ### In Billing Pages/Components ```typescript // ✅ Import from feature import { useInvoices, useInvoice, usePaymentMethods } from "@/features/billing"; // Or direct import import { useInvoices } from "@/features/billing/hooks/useBilling"; function InvoicesPage() { const { data: invoices, isLoading } = useInvoices({ status: "Unpaid" }); // ... } ``` ### In Feature Hooks (within features/billing/hooks/) ```typescript // ✅ Import generic utilities from lib import { apiClient, queryKeys, getDataOrThrow } from "@/lib/api"; // ✅ Import domain types import { type Invoice, type InvoiceList, invoiceSchema, type InvoiceQueryParams } from "@customer-portal/domain/billing"; export function useInvoices(params?: InvoiceQueryParams) { // ... } ``` ### In Other Feature Hooks (features/subscriptions/hooks/) ```typescript // ✅ Generic utilities from lib import { apiClient, queryKeys, getDataOrThrow } from "@/lib/api"; // ✅ Domain types for subscriptions import { type Subscription, subscriptionSchema, type SubscriptionQueryParams } from "@customer-portal/domain/subscriptions"; export function useSubscriptions(params?: SubscriptionQueryParams) { // ... } ``` --- ## 📋 Benefits of This Structure ### 1. **Clear Separation of Concerns** - `api/` - HTTP client & infrastructure - `hooks/` - React Query abstractions - `utils/` - Helper functions ### 2. **Clean Imports** ```typescript // ❌ Before: Messy import { apiClient, queryKeys, getDataOrDefault, getDataOrThrow } from "@/lib/api"; import type { QueryParams } from "@/lib/api/runtime/client"; import { Invoice, InvoiceList } from "@customer-portal/domain/billing"; import { invoiceSchema } from "@customer-portal/domain/validation/shared/entities"; import { INVOICE_STATUS } from "@customer-portal/domain/billing"; // ✅ After: Clean import { apiClient, queryKeys, getDataOrThrow, type QueryParams } from "@/lib/api"; import { type Invoice, type InvoiceList, invoiceSchema, INVOICE_STATUS, } from "@customer-portal/domain/billing"; ``` ### 3. **Easy to Find Things** - Need a query hook? → `lib/hooks/queries/` - Need API utilities? → `lib/api/` - Need to update a domain type? → `packages/domain/billing/` ### 4. **Testable** Each piece can be tested independently: - API helpers are pure functions - Hooks can be tested with React Testing Library - Domain logic is already in domain package --- ## 🔧 What About That Weird Validation Import? ```typescript // ❌ This should NOT exist import { invoiceSchema } from "@customer-portal/domain/validation/shared/entities"; ``` This path suggests you might have old validation code. The schema should be: ```typescript // ✅ Correct import { invoiceSchema } from "@customer-portal/domain/billing"; ``` Let me check if this old validation path exists and needs cleanup.