Skip to main content

Type Safety

No any Types

Use strict TypeScript throughout. Define proper interfaces for all data structures.

Type Locations

FileConventionPurpose
types/database.tssnake_caseMatch Supabase schema
types/api.tscamelCaseAPI request/response contracts
types/forms.tscamelCaseReact Hook Form types

Type Conversion

Database fields use snake_case, API responses use camelCase. Convert with utilities:
import { snakeToCamel, camelToSnake } from '@/lib/utils/typeConversion';

Component Patterns

shadcn/ui Imports

Always import from @/componentlibrary:
import { Button } from '@/componentlibrary/button';
import { Card } from '@/componentlibrary/card';

Feature Components

Location: components/features/{domain}/ where domain is auth/, recipe/, user/. Each component gets its own folder:
components/features/recipe/
├── RecipeCard/
│   ├── RecipeCard.tsx
│   └── index.ts

Icons

Use Lucide React exclusively:
import { Heart, Camera, User, Settings } from 'lucide-react';

Styling

Tailwind CSS only — no CSS Modules. Use cn() for conditional classes:
import { cn } from '@/lib/utils';
<div className={cn("p-4", isActive && "bg-primary")} />

API Route Patterns (Phase 2 — apps/web)

These patterns apply to the web client API routes in apps/web, which is Phase 2. The mobile app calls Supabase directly — it does not use any web API routes.

Authentication

const authResult = await requireAuth(supabase);
if (!authResult.success) return authResult.response;
const { user } = authResult;

Error Responses

Use standardized utilities from @/lib/utils/errors:
return validationError('Invalid data', details);  // 400
return unauthorizedError();                         // 401
return forbiddenError('Not your resource');          // 403
return notFoundError('Recipe');                     // 404
return conflictError('DUPLICATE', 'Already exists'); // 409
return internalError('Something went wrong');        // 500

Validation

Always validate with Zod schemas from @/lib/utils/validation:
const result = recipeSchema.safeParse(body);
if (!result.success) {
  return validationError('Invalid data', result.error.flatten().fieldErrors);
}

Pagination

const validation = validatePaginationParams(pageParam, limitParam);
if (!validation.valid) return validationError(validation.error);

const { page, limit, offset } = parsePagination({ page: pageParam, limit: limitParam });

State Management

LayerToolLocation
Server stateReact Queryhooks/api/
Global stateContext providersAuthContext, ModalContext, ToastContext
Form stateReact Hook Form + Zodcomponents/forms/

Database Conventions

  • Soft deletes: is_deleted flag on recipes, posts, collections
  • Always filter: WHERE is_deleted = FALSE
  • Note: delete_post is a soft delete (is_deleted = true), but delete_recipe is a hard delete (cascading deletion of all related data). Both posts and recipes have the is_deleted column, but the recipe RPC bypasses it.
  • Denormalized counters on profiles table (recipe_count, post_count, etc.) maintained by triggers
  • No database views — all complex queries use RPC functions (see RPC Functions)

Don’ts

  • Use getServerSideProps or getStaticProps (use App Router)
  • Store sensitive data in localStorage
  • Skip confirmation modals for destructive actions
  • Hardcode URLs (use environment variables)
  • Use CSS Modules
  • Create duplicate components
  • Use any type
  • Expose service role key to client