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:
packages/app/client, packages/app/srv, packages/app/db, packages/auth/client, etc.)packages/ (e.g., packages/srv-stripe-payment, packages/likeness-search, packages/social-scraper)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.
The monorepo follows a layered architecture pattern:
Always: Place business logic in packages, never in the apps directory.
This architectural decision maintains loose coupling from Next.js (which has caused issues in the past) and improves code organization by separating client and server concerns into distinct packages.
Note: This is advanced content. Skip this section if you don't currently need to create a new app or package.
# 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
tsconfig.jsonImportant: 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:
package.json as the path keysrc directory of the package/*) to enforce imports through the package's barrel export (index.ts)Note: This step should be done for each package (client, srv, db) as you create them.
packages/my-domain/client/
├── src/
│ ├── components/
│ │ └── MyComponent.tsx
│ └── index.ts
├── dist/ # Generated by tsup
├── package.json
├── tsconfig.json
└── tsup.config.ts
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 conventiontype: "module": Use ESM modulesmain and types: Point to dist/ directoryexports: Define package entry pointpeerDependencies: Framework dependencies (React, Next.js) should be peer depsdevDependencies: Build tools (tsup, typescript)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 referencesdeclarationMap: true and sourceMap: true: Required for IDE navigation to source filesnoEmit: false: Allow TypeScript to emit declarationsoutDir and rootDir: Define source and output directoriestsup.config.tsimport { 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 fileformat: ["esm"]: Output ES modulesexternal: Dependencies that should NOT be bundled (React, etc.)dts: false: Generate declarations separately with tsc (see build script) for better declaration mapssourcemap: true: Generates source maps for debuggingsrc/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";
packages/my-domain/srv/
├── src/
│ └── index.ts
├── dist/ # Generated by tsup
├── package.json
├── tsconfig.json
└── tsup.config.ts
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 conventionpeerDependencies typically needed for server packagesfile: protocoltsconfig.jsonSame 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:
../../../ to extend root tsconfig.json from packages/<domain>/srv/declarationMap: true and sourceMap: true enable proper IDE navigationtsup.config.tsimport { 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:
src/index.ts
export async function myApiFunction() {
return {
message: "Hello from server package!",
timestamp: new Date().toISOString(),
};
}
cd apps/my-app
npx create-next-app@latest .
# Choose: TypeScript, Tailwind CSS, App Router
package.jsonAdd 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:
file: protocol with relative paths to local packagesapps/my-app/ to packages/my-domain/client = ../../packages/my-domain/clientnpm installnext.config.tsConfigure 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 transpilationserverExternalPackages: List server packages that should be externalizedtsconfig.jsonStandard 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:
../../packages/...)/* pattern for sub-path resolutionUsing 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 client package
cd packages/my-domain/client
npm install
npm run build
# Build server package
cd ../srv
npm install
npm run build
# Install app dependencies (this will link packages)
cd apps/my-app
npm install
# Build app
npm run build
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
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.
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.
tsconfig.jsonAdd 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:
./packages/...)/* pattern for sub-path resolutionbaseUrl: "./" is required for paths to work correctlytsconfig.jsonAdd 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:
../../packages/...)@/*) alongside package pathsEnsure 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 filessourceMap: true generates .js.map files for JavaScript source mapssource field helps IDEs locate the original source filestsc separately (tsup's DTS generation may have issues with file inclusion)Restart TypeScript Server: In VS Code/Cursor, press Cmd+Shift+P (Mac) or Ctrl+Shift+P (Windows/Linux) and run "TypeScript: Restart TS Server"
Rebuild Packages: Ensure packages are built with declaration maps:
cd packages/auth/client
npm run build
Test Navigation: Click "Go to Definition" on a package import - it should now navigate to the source file instead of compiled code.
Issue: Still navigating to dist/ files
tsconfig.jsonbaseUrl is set in root tsconfig.jsonls -la packages/auth/client/dist/*.d.ts.mapIssue: TypeScript errors about missing files
src/ directory exists in packages./ for root, ../../ for apps)Issue: Build errors with declaration generation
dts: true fails, use separate tsc command as shown abovetsconfig.json has proper include patternsSolution:
package.json are correct (count directory levels)npm run build in each package)rm -rf node_modules/@zooly && npm installSolution:
import React from "react";external array in tsup.config.tsSolution:
tsconfig.json extends path is correct (../../../tsconfig.json from packages/<domain>/<type>/)tsconfig.json existsnpm install in package directorydeclarationMap and sourceMap are enabled in package tsconfig.jsonSolution:
next.config.ts has correct package namestranspilePackages and serverExternalPackages don't conflictls -la node_modules/@zooly/@zooly/<domain>-client@zooly/<domain>-srv@zooly/<domain>-dbkebab-case (e.g., my-app, zooly-auth)package.json, tsconfig.json, tsup.config.tspackage.json, tsconfig.json, tsup.config.tstsconfig.json paths (see section 2)package.json with correct file: pathsnext.config.ts with transpilePackages and serverExternalPackagestsconfig.json for IDE navigationnpm run build in each)npm install)npm run build in app)npm run dev in app)dist/)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
data-testid attributes on React element divs"type": "module") for all packagesOn This Page
Monorepo StructureBuild Tool: Why tsup?OverviewStep-by-Step: Creating a New App with Packages1. Create the App Directory Structure2. Add Package Paths to Root ,[object Object]3. Create Client Package3.1 Client Package Structure3.2 Client Package: ,[object Object]3.3 Client Package: ,[object Object]3.4 Client Package: ,[object Object]3.5 Client Package: Example Component4. Create Server Package4.1 Server Package Structure4.2 Server Package: ,[object Object]4.3 Server Package: ,[object Object]4.4 Server Package: ,[object Object]4.5 Server Package: Example API Function5. Create Next.js App5.1 Create Next.js App5.2 App: ,[object Object]5.3 App: ,[object Object]5.4 App: ,[object Object]5.5 App: Using PackagesBuild Process1. Build Packages First2. Build App3. Development WorkflowIDE Configuration: TypeScript Paths for "Go to Definition"ProblemSolution: Configure TypeScript Paths1. Root ,[object Object]2. App-Level ,[object Object]3. Package Configuration for Declaration MapsAfter ConfigurationTroubleshootingCommon Issues and SolutionsIssue: Module not found errorsIssue: React is not definedIssue: TypeScript errors in packagesIssue: Next.js can't resolve packagesPackage Naming ConventionsChecklist for New App SetupExample: Complete File StructureAdditional Notes