Monorepo Setup

Understanding the monorepo structure and setting up new packages

Monorepo Structure

The Zooly2 monorepo is organized into two main categories: apps and packages.

zooly2/
├── apps/                    # Next.js or Vite applications
│   └── <app-name>/         # Individual app directory
├── packages/               # Shared, reusable packages
│   ├── <domain>/          # Domain-specific packages (e.g., auth, app, etc.)
│   │   ├── client/        # React components (framework-agnostic)
│   │   ├── srv/           # Server-side API functions
│   │   └── db/            # Database access layer
│   └── <package-name>/    # Standalone packages (e.g., srv-stripe-payment, likeness-search, social-scraper)
└── docs/                  # Documentation

Package Organization:

  • Domain-organized packages: Packages grouped by domain (e.g., packages/app/client, packages/app/srv, packages/app/db, packages/auth/client, etc.)
  • Standalone packages: Independent packages at the root of packages/ (e.g., packages/srv-stripe-payment, packages/likeness-search, packages/social-scraper)

Build Tool: Why tsup?

All packages in this monorepo use tsup as their build tool. tsup precompiles TypeScript to JavaScript and automatically recompiles only changed files during development. This incremental compilation dramatically reduces build times and enables faster iteration when working across multiple packages.

Overview

The monorepo follows a layered architecture pattern:

  • Apps are thin presentation layers (Next.js or Vite) that import and use React components from packages
  • Client packages contain framework-agnostic React code and support Tailwind CSS
  • Server packages handle API logic and depend on db packages for data operations
  • DB packages manage database access and are never directly imported by other packages

Step-by-Step: Creating a New App with Packages

1. Create the App Directory Structure

# Create your app directory
mkdir -p apps/my-app

# Create package directories
mkdir -p packages/my-domain/client
mkdir -p packages/my-domain/srv
mkdir -p packages/my-domain/db

2. Add Package Paths to Root tsconfig.json

Important: After creating each new package, add it to the root tsconfig.json to enable TypeScript path resolution.

Edit the root tsconfig.json file and add a path mapping entry under compilerOptions.paths:

{
  "compilerOptions": {
    "paths": {
      "@zooly/my-domain-client": [
        "./packages/my-domain/client/src"
      ],
      "@zooly/my-domain-srv": [
        "./packages/my-domain/srv/src"
      ],
      "@zooly/my-domain-db": [
        "./packages/my-domain/db/src"
      ]
    }
  }
}

Key Points:

  • Use the package name from package.json as the path key
  • Map to the src directory of the package
  • Only add the base path (without /*) to enforce imports through the package's barrel export (index.ts)
  • This prevents direct access to internal files and maintains proper package boundaries

Note: This step should be done for each package (client, srv, db) as you create them.

3. Create Client Package

3.1 Client Package Structure

packages/my-domain/client/
├── src/
│   ├── components/
│   │   └── MyComponent.tsx
│   └── index.ts
├── dist/                    # Generated by tsup
├── package.json
├── tsconfig.json
└── tsup.config.ts

3.2 Client Package: package.json

{
  "name": "@zooly/my-domain-client",
  "version": "1.0.0",
  "private": true,
  "type": "module",
  "main": "dist/index.js",
  "types": "dist/index.d.ts",
  "source": "src/index.ts",
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "default": "./dist/index.js"
    }
  },
  "scripts": {
    "dev": "tsup --watch",
    "build": "tsup && tsc --emitDeclarationOnly --declarationMap",
    "build:dts": "tsc --emitDeclarationOnly --declarationMap"
  },
  "dependencies": {
    // Add your React dependencies here
  },
  "peerDependencies": {
    "react": "^18.0.0",
    "next": "^15.0.0"
  },
  "devDependencies": {
    "tsup": "^8.0.0",
    "typescript": "^5.0.0"
  }
}

Key Points:

  • name: Use @zooly/<domain>-client naming convention
  • type: "module": Use ESM modules
  • main and types: Point to dist/ directory
  • exports: Define package entry point
  • peerDependencies: Framework dependencies (React, Next.js) should be peer deps
  • devDependencies: Build tools (tsup, typescript)

3.3 Client Package: tsconfig.json

{
  "extends": "../../../tsconfig.json",
  "compilerOptions": {
    "outDir": "dist",
    "rootDir": "src",
    "composite": true,
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true,
    "skipLibCheck": true,
    "noEmit": false,
    "resolveJsonModule": true,
    "tsBuildInfoFile": "./tsconfig.tsbuildinfo",
    "baseUrl": ".",
    "jsx": "react-jsx",
    "paths": {
      "@/*": ["src/*"]
    }
  },
  "include": [
    "src/**/*.ts",
    "src/**/*.tsx",
    "src/**/*.json"
  ],
  "exclude": [
    "node_modules",
    "dist"
  ]
}

Key Points:

  • extends: Extends root tsconfig.json (use ../../../ from packages/<domain>/client/)
  • composite: true: Enables project references
  • declarationMap: true and sourceMap: true: Required for IDE navigation to source files
  • noEmit: false: Allow TypeScript to emit declarations
  • outDir and rootDir: Define source and output directories

3.4 Client Package: tsup.config.ts

import { defineConfig } from "tsup";

export default defineConfig({
  entry: ["src/index.ts"],
  format: ["esm"],
  dts: false, // Generate declarations separately with tsc for better IDE support
  sourcemap: true,
  clean: true,
  external: [
    "react",
    "react-dom",
    // Add other external dependencies that shouldn't be bundled
  ],
});

Key Points:

  • entry: Main entry point file
  • format: ["esm"]: Output ES modules
  • external: Dependencies that should NOT be bundled (React, etc.)
  • dts: false: Generate declarations separately with tsc (see build script) for better declaration maps
  • sourcemap: true: Generates source maps for debugging

3.5 Client Package: Example Component

src/components/MyComponent.tsx

import React from "react";

export function MyComponent() {
  return (
    <div data-testid="my-component" className="p-4 bg-blue-100 rounded-lg">
      <h1 className="text-2xl font-bold">My Component</h1>
    </div>
  );
}

Important: Always include data-testid attributes on React element divs.

src/index.ts

export { MyComponent } from "./components/MyComponent";

4. Create Server Package

4.1 Server Package Structure

packages/my-domain/srv/
├── src/
│   └── index.ts
├── dist/                    # Generated by tsup
├── package.json
├── tsconfig.json
└── tsup.config.ts

4.2 Server Package: package.json

{
  "name": "@zooly/my-domain-srv",
  "version": "1.0.0",
  "private": true,
  "type": "module",
  "main": "dist/index.js",
  "types": "dist/index.d.ts",
  "source": "src/index.ts",
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "default": "./dist/index.js"
    }
  },
  "scripts": {
    "dev": "tsup --watch",
    "build": "tsup && tsc --emitDeclarationOnly --declarationMap",
    "build:dts": "tsc --emitDeclarationOnly --declarationMap"
  },
  "dependencies": {
    // Add server-side dependencies here
    // Example: if using database access, add db package:
    // "@zooly/my-domain-db": "file:../db"
  },
  "devDependencies": {
    "tsup": "^8.0.0",
    "typescript": "^5.0.0"
  }
}

Key Points:

  • name: Use @zooly/<domain>-srv naming convention
  • No peerDependencies typically needed for server packages
  • Can depend on db packages via file: protocol

4.3 Server Package: tsconfig.json

Same structure as client package:

{
  "extends": "../../../tsconfig.json",
  "compilerOptions": {
    "outDir": "dist",
    "rootDir": "src",
    "composite": true,
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true,
    "skipLibCheck": true,
    "noEmit": false,
    "resolveJsonModule": true,
    "tsBuildInfoFile": "./tsconfig.tsbuildinfo",
    "paths": {}
  },
  "include": [
    "src/**/*.ts",
    "src/**/*.tsx",
    "src/**/*.json"
  ],
  "exclude": [
    "node_modules",
    "dist"
  ]
}

Key Points:

  • Use ../../../ to extend root tsconfig.json from packages/<domain>/srv/
  • declarationMap: true and sourceMap: true enable proper IDE navigation

4.4 Server Package: tsup.config.ts

import { defineConfig } from "tsup";

export default defineConfig({
  entry: ["src/index.ts"],
  format: ["esm"],
  dts: false,
  sourcemap: true,
  clean: true,
  external: [
    // Add external dependencies that shouldn't be bundled
    // Example: "googleapis", "@zooly/my-domain-db"
  ],
});

Key Points:

  • Externalize dependencies that should be resolved at runtime
  • Externalize db packages if they're dependencies

4.5 Server Package: Example API Function

src/index.ts

export async function myApiFunction() {
  return {
    message: "Hello from server package!",
    timestamp: new Date().toISOString(),
  };
}

5. Create Next.js App

5.1 Create Next.js App

cd apps/my-app
npx create-next-app@latest .
# Choose: TypeScript, Tailwind CSS, App Router

5.2 App: package.json

Add your packages as dependencies:

{
  "name": "my-app",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "eslint"
  },
  "dependencies": {
    "@zooly/my-domain-client": "file:../../packages/my-domain/client",
    "@zooly/my-domain-srv": "file:../../packages/my-domain/srv",
    "next": "16.1.6",
    "react": "19.2.3",
    "react-dom": "19.2.3"
  },
  "devDependencies": {
    "@tailwindcss/postcss": "^4",
    "@types/node": "^20",
    "@types/react": "^19",
    "@types/react-dom": "^19",
    "eslint": "^9",
    "eslint-config-next": "16.1.6",
    "tailwindcss": "^4",
    "typescript": "^5"
  }
}

Key Points:

  • Use file: protocol with relative paths to local packages
  • Path calculation: From apps/my-app/ to packages/my-domain/client = ../../packages/my-domain/client
  • Install dependencies: npm install

5.3 App: next.config.ts

Configure Next.js to handle your packages:

import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  // Transpile client packages (React components)
  transpilePackages: ["@zooly/my-domain-client"],
  
  // Externalize server packages (they run in Node.js, not bundled)
  serverExternalPackages: ["@zooly/my-domain-srv"],
};

export default nextConfig;

Key Points:

  • transpilePackages: List client packages that need transpilation
  • serverExternalPackages: List server packages that should be externalized
  • Important: Don't list the same package in both arrays

5.4 App: tsconfig.json

Standard Next.js tsconfig with package path mappings:

{
  "compilerOptions": {
    "target": "ES2017",
    "lib": ["dom", "dom.iterable", "esnext"],
    "allowJs": true,
    "skipLibCheck": true,
    "strict": true,
    "noEmit": true,
    "esModuleInterop": true,
    "module": "esnext",
    "moduleResolution": "bundler",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "react-jsx",
    "incremental": true,
    "plugins": [
      {
        "name": "next"
      }
    ],
    "paths": {
      "@/*": ["./*"],
      "@zooly/my-domain-client": ["../../packages/my-domain/client/src"],
      "@zooly/my-domain-client/*": ["../../packages/my-domain/client/src/*"],
      "@zooly/my-domain-srv": ["../../packages/my-domain/srv/src"],
      "@zooly/my-domain-srv/*": ["../../packages/my-domain/srv/src/*"]
    }
  },
  "include": [
    "next-env.d.ts",
    "**/*.ts",
    "**/*.tsx",
    ".next/types/**/*.ts",
    ".next/dev/types/**/*.ts",
    "**/*.mts"
  ],
  "exclude": ["node_modules"]
}

Key Points:

  • Add package path mappings to enable "Go to Definition" navigation to source files
  • Paths are relative to the app directory (../../packages/...)
  • Include both package name and /* pattern for sub-path resolution

5.5 App: Using Packages

Using Client Package in a Page:

app/page.tsx

"use client";

import { MyComponent } from "@zooly/my-domain-client";

export default function Home() {
  return (
    <div>
      <MyComponent />
    </div>
  );
}

Using Server Package in API Route:

app/api/my-api/route.ts

import { myApiFunction } from "@zooly/my-domain-srv";
import { NextResponse } from "next/server";

export async function GET() {
  const result = await myApiFunction();
  return NextResponse.json(result);
}

Build Process

1. Build Packages First

# Build client package
cd packages/my-domain/client
npm install
npm run build

# Build server package
cd ../srv
npm install
npm run build

2. Build App

# Install app dependencies (this will link packages)
cd apps/my-app
npm install

# Build app
npm run build

3. Development Workflow

Option 1: Watch Mode (Recommended)

Terminal 1 - Watch client package:

cd packages/my-domain/client
npm run dev  # Runs tsup --watch

Terminal 2 - Watch server package:

cd packages/my-domain/srv
npm run dev  # Runs tsup --watch

Terminal 3 - Run Next.js app:

cd apps/my-app
npm run dev

Option 2: Build Before Dev

Build packages once, then run app:

# Build packages
cd packages/my-domain/client && npm run build
cd ../srv && npm run build

# Run app
cd ../../apps/my-app && npm run dev

IDE Configuration: TypeScript Paths for "Go to Definition"

To enable proper IDE navigation (e.g., "Go to Definition" opening source files instead of compiled dist/ files), you need to configure TypeScript paths in both the root and app-level tsconfig.json files.

Problem

By default, when you click "Go to Definition" on a package import like @zooly/auth-client, your IDE may navigate to the compiled dist/index.js file instead of the source src/components/auth/LoginPage.tsx file.

Solution: Configure TypeScript Paths

1. Root tsconfig.json

Add path mappings for all packages in the root tsconfig.json:

{
  "compilerOptions": {
    // ... other options ...
    "paths": {
      "@zooly/auth-client": ["./packages/auth/client/src"],
      "@zooly/auth-client/*": ["./packages/auth/client/src/*"],
      "@zooly/auth-srv": ["./packages/auth/srv/src"],
      "@zooly/auth-srv/*": ["./packages/auth/srv/src/*"]
      // Add more packages as needed
    },
    "baseUrl": "./"
  }
}

Key Points:

  • Paths are relative to the root directory (./packages/...)
  • Include both the package name and /* pattern for sub-path resolution
  • baseUrl: "./" is required for paths to work correctly

2. App-Level tsconfig.json

Add the same path mappings to your app's tsconfig.json, but use relative paths from the app directory:

{
  "compilerOptions": {
    // ... other options ...
    "paths": {
      "@/*": ["./*"],
      "@zooly/auth-client": ["../../packages/auth/client/src"],
      "@zooly/auth-client/*": ["../../packages/auth/client/src/*"],
      "@zooly/auth-srv": ["../../packages/auth/srv/src"],
      "@zooly/auth-srv/*": ["../../packages/auth/srv/src/*"]
    }
  }
}

Key Points:

  • Paths are relative to the app directory (../../packages/...)
  • Keep your app-specific paths (like @/*) alongside package paths

3. Package Configuration for Declaration Maps

Ensure your packages generate declaration maps for proper source navigation:

Package tsconfig.json:

{
  "extends": "../../../tsconfig.json",
  "compilerOptions": {
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true,
    // ... other options ...
  }
}

Package tsup.config.ts:

import { defineConfig } from "tsup";

export default defineConfig({
  entry: ["src/index.ts"],
  format: ["esm"],
  dts: false, // Generate declarations separately with tsc
  sourcemap: true,
  // ... other options ...
});

Package package.json scripts:

{
  "scripts": {
    "build": "tsup && tsc --emitDeclarationOnly --declarationMap",
    "build:dts": "tsc --emitDeclarationOnly --declarationMap"
  }
}

Key Points:

  • declarationMap: true generates .d.ts.map files that map declarations to source files
  • sourceMap: true generates .js.map files for JavaScript source maps
  • source field helps IDEs locate the original source files
  • Generate declarations with tsc separately (tsup's DTS generation may have issues with file inclusion)

After Configuration

  1. Restart TypeScript Server: In VS Code/Cursor, press Cmd+Shift+P (Mac) or Ctrl+Shift+P (Windows/Linux) and run "TypeScript: Restart TS Server"

  2. Rebuild Packages: Ensure packages are built with declaration maps:

    cd packages/auth/client
    npm run build
  3. Test Navigation: Click "Go to Definition" on a package import - it should now navigate to the source file instead of compiled code.

Troubleshooting

Issue: Still navigating to dist/ files

  • Verify paths are correct in both root and app tsconfig.json
  • Ensure baseUrl is set in root tsconfig.json
  • Restart TypeScript server
  • Check that declaration maps exist: ls -la packages/auth/client/dist/*.d.ts.map

Issue: TypeScript errors about missing files

  • Verify path patterns match your package structure
  • Ensure src/ directory exists in packages
  • Check that paths use correct relative notation (./ for root, ../../ for apps)

Issue: Build errors with declaration generation

  • If tsup's dts: true fails, use separate tsc command as shown above
  • Ensure tsconfig.json has proper include patterns

Common Issues and Solutions

Issue: Module not found errors

Solution:

  1. Verify package paths in package.json are correct (count directory levels)
  2. Ensure packages are built (npm run build in each package)
  3. Reinstall app dependencies: rm -rf node_modules/@zooly && npm install

Issue: React is not defined

Solution:

  • Ensure React is imported in client package components: import React from "react";
  • Add React to external array in tsup.config.ts

Issue: TypeScript errors in packages

Solution:

  • Check tsconfig.json extends path is correct (../../../tsconfig.json from packages/<domain>/<type>/)
  • Ensure root tsconfig.json exists
  • Run npm install in package directory
  • Verify declarationMap and sourceMap are enabled in package tsconfig.json

Issue: Next.js can't resolve packages

Solution:

  • Verify next.config.ts has correct package names
  • Check that transpilePackages and serverExternalPackages don't conflict
  • Ensure symlinks exist: ls -la node_modules/@zooly/

Package Naming Conventions

  • Client packages: @zooly/<domain>-client
  • Server packages: @zooly/<domain>-srv
  • DB packages: @zooly/<domain>-db
  • App names: kebab-case (e.g., my-app, zooly-auth)

Checklist for New App Setup

  • Create app directory structure
  • Create client package with proper package.json, tsconfig.json, tsup.config.ts
  • Create server package with proper package.json, tsconfig.json, tsup.config.ts
  • Add each package to root tsconfig.json paths (see section 2)
  • Create Next.js app
  • Add packages to app's package.json with correct file: paths
  • Configure next.config.ts with transpilePackages and serverExternalPackages
  • Configure TypeScript paths in app tsconfig.json for IDE navigation
  • Build all packages (npm run build in each)
  • Install app dependencies (npm install)
  • Test build (npm run build in app)
  • Test dev server (npm run dev in app)
  • Verify "Go to Definition" navigates to source files (not dist/)

Example: Complete File Structure

zooly2/
├── apps/
│   └── my-app/
│       ├── app/
│       │   ├── api/
│       │   │   └── my-api/
│       │   │       └── route.ts
│       │   └── page.tsx
│       ├── package.json
│       ├── next.config.ts
│       └── tsconfig.json
└── packages/
    └── my-domain/
        ├── client/
        │   ├── src/
        │   │   ├── components/
        │   │   │   └── MyComponent.tsx
        │   │   └── index.ts
        │   ├── dist/
        │   ├── package.json
        │   ├── tsconfig.json
        │   └── tsup.config.ts
        └── srv/
            ├── src/
            │   └── index.ts
            ├── dist/
            ├── package.json
            ├── tsconfig.json
            └── tsup.config.ts

Additional Notes

  • Client packages should be framework-agnostic but can use Tailwind CSS
  • Server packages handle API logic and can depend on db packages
  • DB packages manage database access and should NOT expose tables directly
  • Always include data-testid attributes on React element divs
  • Use ESM modules ("type": "module") for all packages
  • Build packages before building the app
  • Use watch mode during development for faster iteration