Replace tracked symlinks with files
This commit is contained in:
@@ -1 +0,0 @@
|
||||
../agents/rules
|
||||
@@ -0,0 +1,79 @@
|
||||
# Cal.diy Engineering Rules
|
||||
|
||||
This directory contains modular, machine-readable engineering rules derived from [Cal.diy's Engineering Standards for 2026 and Beyond](https://cal.com/blog/engineering-in-2026-and-beyond).
|
||||
|
||||
## Structure
|
||||
|
||||
Rules are organized by section prefix, as defined in `_sections.md`:
|
||||
|
||||
| Prefix | Section | Impact |
|
||||
|--------|---------|--------|
|
||||
| `architecture-` | Architecture | CRITICAL |
|
||||
| `quality-` | Code Quality | CRITICAL |
|
||||
| `data-` | Data Layer | HIGH |
|
||||
| `api-` | API Design | HIGH |
|
||||
| `performance-` | Performance | HIGH |
|
||||
| `testing-` | Testing | MEDIUM-HIGH |
|
||||
| `patterns-` | Design Patterns | MEDIUM |
|
||||
| `culture-` | Team Culture | MEDIUM |
|
||||
|
||||
## Files
|
||||
|
||||
- `_sections.md` - Defines all sections, their ordering, and impact levels
|
||||
- `_template.md` - Template for creating new rules
|
||||
- `{section}-{rule-name}.md` - Individual rule files
|
||||
|
||||
## Rule Format
|
||||
|
||||
Each rule file follows a consistent format with YAML frontmatter:
|
||||
|
||||
```markdown
|
||||
---
|
||||
title: Rule Title Here
|
||||
impact: CRITICAL | HIGH | MEDIUM | LOW
|
||||
impactDescription: Optional description (e.g., "20-50% improvement")
|
||||
tags: tag1, tag2, tag3
|
||||
---
|
||||
|
||||
## Rule Title Here
|
||||
|
||||
**Impact: LEVEL (optional description)**
|
||||
|
||||
Brief explanation of the rule and why it matters.
|
||||
|
||||
**Incorrect (description):**
|
||||
\`\`\`typescript
|
||||
// Bad code example
|
||||
\`\`\`
|
||||
|
||||
**Correct (description):**
|
||||
\`\`\`typescript
|
||||
// Good code example
|
||||
\`\`\`
|
||||
|
||||
Reference: [Link](url)
|
||||
```
|
||||
|
||||
## Adding New Rules
|
||||
|
||||
1. Copy `_template.md` to a new file with the appropriate section prefix
|
||||
2. Fill in the frontmatter (title, impact, tags)
|
||||
3. Write a clear explanation of the rule
|
||||
4. Provide incorrect and correct code examples
|
||||
5. Add a reference link if applicable
|
||||
|
||||
## Usage
|
||||
|
||||
These rules are designed to be:
|
||||
- **Human-readable**: Engineers can browse and learn from them
|
||||
- **Machine-readable**: AI agents can parse and apply them
|
||||
- **Modular**: Individual rules can be updated without affecting others
|
||||
- **Versionable**: Changes are tracked in git history
|
||||
|
||||
## Core Principles
|
||||
|
||||
From the blog post, our engineering philosophy is:
|
||||
|
||||
> We are building infrastructure that must almost never fail. To achieve this, we move fast while shipping amazing quality software with no shortcuts or compromises.
|
||||
|
||||
The rules in this directory encode the specific practices that enable this philosophy.
|
||||
@@ -0,0 +1,56 @@
|
||||
# Sections
|
||||
|
||||
This file defines all sections, their ordering, impact levels, and descriptions.
|
||||
The section ID (in parentheses) is the filename prefix used to group rules.
|
||||
|
||||
---
|
||||
|
||||
## 1. Architecture (architecture)
|
||||
|
||||
**Impact:** CRITICAL
|
||||
**Description:** Vertical Slice Architecture and Domain-Driven Design patterns that form the foundation of our codebase organization.
|
||||
|
||||
## 2. Code Quality (quality)
|
||||
|
||||
**Impact:** CRITICAL
|
||||
**Description:** Standards for maintaining high-quality, maintainable code including PR reviews, testing, and accountability.
|
||||
|
||||
## 3. Data Layer (data)
|
||||
|
||||
**Impact:** HIGH
|
||||
**Description:** Repository patterns, DTOs, and technology isolation to prevent coupling and enable maintainability.
|
||||
|
||||
## 4. API Design (api)
|
||||
|
||||
**Impact:** HIGH
|
||||
**Description:** Controller patterns, API stability, and proper separation of HTTP concerns from business logic.
|
||||
|
||||
## 5. Performance (performance)
|
||||
|
||||
**Impact:** HIGH
|
||||
**Description:** Algorithm complexity, data structure choices, and performance patterns for enterprise scale.
|
||||
|
||||
## 6. Testing (testing)
|
||||
|
||||
**Impact:** MEDIUM-HIGH
|
||||
**Description:** Test coverage requirements and testing strategies for maintaining code quality.
|
||||
|
||||
## 7. Design Patterns (patterns)
|
||||
|
||||
**Impact:** MEDIUM
|
||||
**Description:** Factory patterns, dependency injection, and other design patterns for clean, maintainable code.
|
||||
|
||||
## 8. Team Culture (culture)
|
||||
|
||||
**Impact:** MEDIUM
|
||||
**Description:** Engineering culture, accountability, and collaboration standards.
|
||||
|
||||
## 9. CI/CD (ci)
|
||||
|
||||
**Impact:** HIGH
|
||||
**Description:** Continuous integration practices, type checking priorities, and git workflow standards.
|
||||
|
||||
## 10. Reference (reference)
|
||||
|
||||
**Impact:** LOW
|
||||
**Description:** Informational lookups, file locations, and local development setup guides.
|
||||
@@ -0,0 +1,28 @@
|
||||
---
|
||||
title: Rule Title Here
|
||||
impact: MEDIUM
|
||||
impactDescription: Optional description of impact (e.g., "20-50% improvement")
|
||||
tags: tag1, tag2
|
||||
---
|
||||
|
||||
## Rule Title Here
|
||||
|
||||
**Impact: MEDIUM (optional impact description)**
|
||||
|
||||
Brief explanation of the rule and why it matters. This should be clear and concise, explaining the implications.
|
||||
|
||||
**Incorrect (description of what's wrong):**
|
||||
|
||||
```typescript
|
||||
// Bad code example here
|
||||
const bad = example()
|
||||
```
|
||||
|
||||
**Correct (description of what's right):**
|
||||
|
||||
```typescript
|
||||
// Good code example here
|
||||
const good = example()
|
||||
```
|
||||
|
||||
Reference: [Link to documentation or resource](https://example.com)
|
||||
@@ -0,0 +1,60 @@
|
||||
---
|
||||
title: Never Introduce Breaking API Changes
|
||||
impact: CRITICAL
|
||||
impactDescription: Maintains developer trust and prevents integration nightmares
|
||||
tags: api, stability, versioning, backwards-compatibility
|
||||
---
|
||||
|
||||
## Never Introduce Breaking API Changes
|
||||
|
||||
**Impact: CRITICAL**
|
||||
|
||||
Once an API endpoint is public, it must remain stable. Breaking changes destroy developer trust and create integration nightmares for our users.
|
||||
|
||||
**Strategies for avoiding breaking changes:**
|
||||
- Always add new fields as optional
|
||||
- Use API versioning when you must change existing behavior
|
||||
- Deprecate old endpoints gracefully with clear migration paths
|
||||
- Maintain backward compatibility for at least two major versions
|
||||
|
||||
**Incorrect (breaking change):**
|
||||
|
||||
```typescript
|
||||
// v1 - Original response
|
||||
interface BookingResponse {
|
||||
id: number;
|
||||
startTime: string; // ISO string
|
||||
}
|
||||
|
||||
// v1 - Breaking change: renamed field
|
||||
interface BookingResponse {
|
||||
id: number;
|
||||
start: string; // Renamed from startTime - BREAKS CLIENTS
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (non-breaking evolution):**
|
||||
|
||||
```typescript
|
||||
// v1 - Original response
|
||||
interface BookingResponse {
|
||||
id: number;
|
||||
startTime: string;
|
||||
}
|
||||
|
||||
// v1 - Non-breaking: add new field, keep old one
|
||||
interface BookingResponse {
|
||||
id: number;
|
||||
startTime: string; // Keep for backwards compatibility
|
||||
start: string; // New preferred field
|
||||
}
|
||||
```
|
||||
|
||||
**When you must make breaking changes:**
|
||||
- Create a new API version using date-specific versioning in API v2
|
||||
- Run both versions simultaneously during transition
|
||||
- Provide automated migration tools when possible
|
||||
- Give users ample time to migrate (minimum 6 months for public APIs)
|
||||
- Document exactly what changed and why
|
||||
|
||||
Reference: [Cal.diy Engineering Blog](https://cal.com/blog/engineering-in-2026-and-beyond)
|
||||
@@ -0,0 +1,73 @@
|
||||
---
|
||||
title: Keep Controllers Thin - HTTP Concerns Only
|
||||
impact: HIGH
|
||||
impactDescription: Enables technology-agnostic business logic
|
||||
tags: api, controllers, http, separation-of-concerns
|
||||
---
|
||||
|
||||
## Keep Controllers Thin - HTTP Concerns Only
|
||||
|
||||
**Impact: HIGH**
|
||||
|
||||
Controllers are thin layers that handle only HTTP concerns. They take requests, process them, and map data to DTOs that are passed to core application logic. No application or core logic should be seen in API routes or tRPC handlers.
|
||||
|
||||
**Controller responsibilities (and ONLY these):**
|
||||
- Receive and validate incoming requests
|
||||
- Extract data from request parameters, body, headers
|
||||
- Transform request data into DTOs
|
||||
- Call appropriate application services with those DTOs
|
||||
- Transform application service responses into response DTOs
|
||||
- Return HTTP responses with proper status codes
|
||||
|
||||
**Controllers should NOT:**
|
||||
- Contain business logic or domain rules
|
||||
- Directly access databases or external services
|
||||
- Perform complex data transformations or calculations
|
||||
- Make decisions about what the application should do
|
||||
- Know about implementation details of the domain
|
||||
|
||||
**Incorrect (business logic in controller):**
|
||||
|
||||
```typescript
|
||||
export async function POST(request: Request) {
|
||||
const body = await request.json();
|
||||
|
||||
// Business logic in controller - BAD
|
||||
const user = await prisma.user.findFirst({ where: { id: body.userId } });
|
||||
if (!user.canBook) {
|
||||
return Response.json({ error: "Cannot book" }, { status: 403 });
|
||||
}
|
||||
|
||||
const booking = await prisma.booking.create({
|
||||
data: {
|
||||
title: body.title,
|
||||
startTime: new Date(body.startTime),
|
||||
// Complex logic here...
|
||||
}
|
||||
});
|
||||
|
||||
return Response.json(booking);
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (thin controller):**
|
||||
|
||||
```typescript
|
||||
export async function POST(request: Request) {
|
||||
const body = await request.json();
|
||||
|
||||
// Validate input
|
||||
const input = CreateBookingSchema.parse(body);
|
||||
|
||||
// Delegate to service
|
||||
const result = await bookingService.createBooking(input);
|
||||
|
||||
// Transform and return
|
||||
return Response.json(BookingResponseSchema.parse(result));
|
||||
}
|
||||
```
|
||||
|
||||
**The principle:**
|
||||
We must detach HTTP technology from our application. The way we transfer data between client and server (whether REST, tRPC, etc.) should not influence how our core application works. HTTP is a delivery mechanism, not an architectural driver.
|
||||
|
||||
Reference: [Cal.diy Engineering Blog](https://cal.com/blog/engineering-in-2026-and-beyond)
|
||||
@@ -0,0 +1,193 @@
|
||||
---
|
||||
title: Prevent Circular Dependencies Between Packages
|
||||
impact: CRITICAL
|
||||
impactDescription: Prevents build failures, type errors, ensures honest implementation, and maintains clean dependency graph
|
||||
tags: architecture, dependencies, packages, imports, circular
|
||||
---
|
||||
|
||||
## Prevent Circular Dependencies Between Packages
|
||||
|
||||
**Impact: CRITICAL**
|
||||
|
||||
Circular dependencies between packages cause build failures, type errors, and make the codebase unmaintainable. The dependency graph must be acyclic, with clear layers from low-level utilities up to high-level features.
|
||||
|
||||
The dependency hierarchy (from lowest to highest):
|
||||
```
|
||||
packages/lib (lowest - no feature dependencies)
|
||||
↓
|
||||
packages/app-store
|
||||
↓
|
||||
packages/features
|
||||
↓
|
||||
packages/trpc
|
||||
↓
|
||||
apps/web (highest - can depend on all packages)
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- **Honest implementation**: The dependency graph accurately reflects what code depends on what, with no hidden or circular relationships that create false expectations
|
||||
- **Predictable behavior**: Code behaves as the dependency structure suggests - no surprises from circular imports
|
||||
- **Clear mental model**: Developers can reason about the system without tracking complex circular relationships
|
||||
- **Build reliability**: No circular dependency errors during compilation or bundling
|
||||
- **Easier testing**: Lower-level packages can be tested independently without pulling in the entire codebase
|
||||
|
||||
### `packages/lib`
|
||||
|
||||
**Rules:**
|
||||
1. No files in `packages/lib` import from `@calcom/app-store` or `../app-store/**`
|
||||
2. No files in `packages/lib` import from `@calcom/features` or `../features/**`
|
||||
3. No files in `packages/lib` import from `@calcom/trpc` or `../trpc/**`
|
||||
4. No files in `packages/lib` import from `@trpc/server`
|
||||
5. **BONUS:** No repository files allowed in `packages/lib` (repositories belong in features or app-store)
|
||||
|
||||
**Incorrect:**
|
||||
|
||||
```typescript
|
||||
// Bad - lib importing from features
|
||||
import { getBooking } from "@calcom/features/bookings";
|
||||
import { EventRepository } from "@calcom/features/events/repositories";
|
||||
|
||||
// Bad - lib importing from app-store
|
||||
import { googleCalendarHandler } from "@calcom/app-store/googlecalendar";
|
||||
|
||||
// Bad - lib importing from trpc
|
||||
import { router } from "@calcom/trpc/server/trpc";
|
||||
```
|
||||
|
||||
**Correct:**
|
||||
|
||||
```typescript
|
||||
// Good - lib only imports from other lib files or external packages
|
||||
import { logger } from "@calcom/lib/logger";
|
||||
import { prisma } from "@calcom/prisma";
|
||||
import type { User } from "@prisma/client";
|
||||
```
|
||||
|
||||
### `packages/app-store`
|
||||
|
||||
**Rules:**
|
||||
6. No files in `packages/app-store` import from `@calcom/features` or `../features/**`
|
||||
7. No files in `packages/app-store` import from `@calcom/trpc` or `../trpc/**`
|
||||
|
||||
**Incorrect:**
|
||||
|
||||
```typescript
|
||||
// Bad - app-store importing from features
|
||||
import { BookingService } from "@calcom/features/bookings/services";
|
||||
|
||||
// Bad - app-store importing from trpc
|
||||
import { publicProcedure } from "@calcom/trpc/server/trpc";
|
||||
```
|
||||
|
||||
**Correct:**
|
||||
|
||||
```typescript
|
||||
// Good - app-store imports from lib and prisma
|
||||
import { logger } from "@calcom/lib/logger";
|
||||
import { prisma } from "@calcom/prisma";
|
||||
import type { Credential } from "@prisma/client";
|
||||
```
|
||||
|
||||
### `packages/features`
|
||||
|
||||
**Rules:**
|
||||
8. No files in `packages/features` import from `@calcom/trpc` or `../trpc/**`
|
||||
9. No files in `packages/features` import from `@calcom/web` or `@calcom/web/**`
|
||||
10. No files in `packages/features` import from `../../apps/web/**`
|
||||
|
||||
**Incorrect:**
|
||||
|
||||
```typescript
|
||||
// Bad - features importing from trpc
|
||||
import { router } from "@calcom/trpc/server/trpc";
|
||||
|
||||
// Bad - features importing from web app
|
||||
import { getServerSession } from "@calcom/web/lib/auth";
|
||||
import { WebComponent } from "../../apps/web/components/WebComponent";
|
||||
```
|
||||
|
||||
**Correct:**
|
||||
|
||||
```typescript
|
||||
// Good - features import from lib, app-store, and prisma
|
||||
import { logger } from "@calcom/lib/logger";
|
||||
import { prisma } from "@calcom/prisma";
|
||||
import { googleCalendarHandler } from "@calcom/app-store/googlecalendar";
|
||||
```
|
||||
|
||||
### `packages/trpc`
|
||||
|
||||
**Rules:**
|
||||
11. No files in `packages/trpc` import from `../apps/web/**`
|
||||
|
||||
**Incorrect:**
|
||||
|
||||
```typescript
|
||||
// Bad - trpc importing from web app
|
||||
import { getServerSession } from "../../apps/web/lib/auth";
|
||||
```
|
||||
|
||||
**Correct:**
|
||||
|
||||
```typescript
|
||||
// Good - trpc imports from features, lib, and prisma
|
||||
import { BookingService } from "@calcom/features/bookings/services";
|
||||
import { logger } from "@calcom/lib/logger";
|
||||
import { prisma } from "@calcom/prisma";
|
||||
```
|
||||
|
||||
### `packages/testing`
|
||||
|
||||
**Rules:**
|
||||
12. No files in `packages/testing` import from `@calcom/web` or `@calcom/web/**`
|
||||
13. No files in `packages/testing` import from `@calcom/features` or `@calcom/features/**`
|
||||
14. No files in `packages/testing` import from `../../apps/web/**`
|
||||
15. No files in `packages/testing` import from `../features/**`
|
||||
|
||||
**Incorrect:**
|
||||
|
||||
```typescript
|
||||
// Bad - testing importing from web or features
|
||||
import { WebComponent } from "@calcom/web/components";
|
||||
import { BookingService } from "@calcom/features/bookings";
|
||||
```
|
||||
|
||||
**Correct:**
|
||||
|
||||
```typescript
|
||||
// Good - testing imports from lib and prisma
|
||||
import { prisma } from "@calcom/prisma";
|
||||
import { logger } from "@calcom/lib/logger";
|
||||
```
|
||||
|
||||
### `packages/platform/atoms`
|
||||
|
||||
**Rules:**
|
||||
16. No files in `packages/platform/atoms` import from `@calcom/trpc` or `@calcom/trpc/**`
|
||||
17. No files in `packages/platform/atoms` import from `../../trpc` or `../../trpc/**`
|
||||
18. No files in `packages/platform/atoms` import from `@calcom/web`
|
||||
|
||||
**Incorrect:**
|
||||
|
||||
```typescript
|
||||
// Bad - platform/atoms importing from trpc or web
|
||||
import { router } from "@calcom/trpc/server/trpc";
|
||||
import { trpc } from "../../trpc";
|
||||
import { WebComponent } from "@calcom/web";
|
||||
```
|
||||
|
||||
**Correct:**
|
||||
|
||||
```typescript
|
||||
// Good - platform/atoms imports from lib, features, and ui
|
||||
import { logger } from "@calcom/lib/logger";
|
||||
import { Button } from "@calcom/ui/components/button";
|
||||
import type { Booking } from "@calcom/features/bookings/types";
|
||||
```
|
||||
|
||||
## Enforcement
|
||||
|
||||
These rules should be enforced through:
|
||||
- ESLint rules that detect forbidden import paths
|
||||
- CI checks that fail on circular dependencies
|
||||
- Code review guidelines that flag violations
|
||||
@@ -0,0 +1,42 @@
|
||||
---
|
||||
title: Enforce Feature Boundaries Through Public APIs
|
||||
impact: CRITICAL
|
||||
impactDescription: Prevents architectural erosion and maintains loose coupling
|
||||
tags: architecture, boundaries, imports, coupling
|
||||
---
|
||||
|
||||
## Enforce Feature Boundaries Through Public APIs
|
||||
|
||||
**Impact: CRITICAL**
|
||||
|
||||
Features communicate through well-defined interfaces. If bookings needs availability data, it imports from `@calcom/features/availability` through exported interfaces, not by reaching into internal implementation details.
|
||||
|
||||
**Incorrect (reaching into internals):**
|
||||
|
||||
```typescript
|
||||
// Bad - Importing internal implementation details
|
||||
import { calculateSlots } from "@calcom/features/availability/services/internal/slotCalculator";
|
||||
import { AvailabilityCache } from "@calcom/features/availability/lib/cache";
|
||||
```
|
||||
|
||||
**Correct (using public API):**
|
||||
|
||||
```typescript
|
||||
// Good - Import through the feature's public API
|
||||
import { getAvailability } from "@calcom/features/availability";
|
||||
import type { AvailabilityResult } from "@calcom/features/availability";
|
||||
```
|
||||
|
||||
**Shared code placement:**
|
||||
- Domain-agnostic utilities and cross-cutting concerns (auth, logging): `packages/lib`
|
||||
- Shared UI primitives: `packages/ui`
|
||||
|
||||
**Enforcement:**
|
||||
Domain boundaries are enforced automatically through linting. If `packages/features/bookings` tries to import from `packages/features/availability/services/internal`, the linter will block it. All cross-feature dependencies must go through the feature's public API.
|
||||
|
||||
**Benefits:**
|
||||
- Discoverability: Looking for booking logic? It's all in `packages/features/bookings`
|
||||
- Easier testing: Test the entire feature as a unit with all pieces in one place
|
||||
- Clearer dependencies: When you see `import { getAvailability } from '@calcom/features/availability'`, you know exactly which feature you're depending on
|
||||
|
||||
Reference: [Cal.diy Engineering Blog](https://cal.com/blog/engineering-in-2026-and-beyond)
|
||||
@@ -0,0 +1,54 @@
|
||||
---
|
||||
title: packages/features vs apps/web/modules
|
||||
impact: HIGH
|
||||
impactDescription: Wrong placement causes tight coupling and import issues
|
||||
tags: architecture, features, modules, trpc
|
||||
---
|
||||
|
||||
# packages/features vs apps/web/modules
|
||||
|
||||
## packages/features
|
||||
|
||||
The `packages/features` package should contain only framework-agnostic code:
|
||||
- Repositories (data access layer)
|
||||
- Services (business logic)
|
||||
- Core utilities and helpers
|
||||
- Types and interfaces
|
||||
|
||||
**Files in `packages/features/**` should NOT import from `@calcom/trpc`.**
|
||||
|
||||
## apps/web/modules
|
||||
|
||||
Web-specific code, particularly anything that uses tRPC, should live in `apps/web/modules/...`:
|
||||
- React hooks that use tRPC queries/mutations
|
||||
- tRPC-specific utilities
|
||||
- Web-only UI components that depend on tRPC
|
||||
|
||||
## Example Structure
|
||||
|
||||
```
|
||||
packages/features/feature-opt-in/
|
||||
├── repository/
|
||||
│ └── FeatureOptInRepository.ts # Data access - OK here
|
||||
├── service/
|
||||
│ └── FeatureOptInService.ts # Business logic - OK here
|
||||
└── types.ts # Types - OK here
|
||||
|
||||
apps/web/modules/feature-opt-in/
|
||||
└── hooks/
|
||||
└── useFeatureOptIn.ts # tRPC hook - MUST be here
|
||||
```
|
||||
|
||||
## Why This Matters
|
||||
|
||||
```typescript
|
||||
// ❌ Bad - tRPC hook in packages/features
|
||||
// packages/features/feature-opt-in/hooks/useFeatureOptIn.ts
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
|
||||
// ✅ Good - tRPC hook in apps/web/modules
|
||||
// apps/web/modules/feature-opt-in/hooks/useFeatureOptIn.ts
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
```
|
||||
|
||||
This separation ensures that `packages/features` remains portable and can be used by other apps (like `apps/api/v2`) without pulling in web-specific dependencies.
|
||||
@@ -0,0 +1,56 @@
|
||||
---
|
||||
title: Page-Level Authorization Checks in Next.js
|
||||
impact: CRITICAL
|
||||
impactDescription: Prevents unauthorized access to sensitive data
|
||||
tags: security, nextjs, authorization, architecture
|
||||
---
|
||||
|
||||
## Page-Level Authorization Checks in Next.js
|
||||
|
||||
**Impact: CRITICAL (Prevents unauthorized access to sensitive data)**
|
||||
|
||||
Authorization checks must be performed in `page.tsx` or server components, never in `layout.tsx`. Layouts don't intercept all requests and can be bypassed.
|
||||
|
||||
**Incorrect (auth checks in layout):**
|
||||
|
||||
```tsx
|
||||
// app/admin/layout.tsx - DON'T DO THIS
|
||||
export default async function AdminLayout({ children }) {
|
||||
const session = await getUserSession();
|
||||
if (!session?.user.role === "admin") {
|
||||
redirect("/");
|
||||
}
|
||||
return <div>{children}</div>;
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (auth checks in page):**
|
||||
|
||||
```tsx
|
||||
// app/admin/page.tsx
|
||||
import { redirect } from "next/navigation";
|
||||
import { getUserSession } from "@/lib/auth";
|
||||
|
||||
export default async function AdminPage() {
|
||||
const session = await getUserSession();
|
||||
|
||||
if (!session || session.user.role !== "admin") {
|
||||
redirect("/"); // Or show an error
|
||||
}
|
||||
|
||||
// Protected content here
|
||||
return <div>Welcome, Admin!</div>;
|
||||
}
|
||||
```
|
||||
|
||||
**Why layouts are unsafe for auth:**
|
||||
- Layouts don't intercept all requests (direct navigation, refreshes)
|
||||
- APIs and server actions bypass layouts entirely
|
||||
- Risk of data leaks if layout check is skipped
|
||||
|
||||
**Key rules:**
|
||||
- Check permissions inside every restricted `page.tsx`
|
||||
- Validate session/user/role before querying sensitive data
|
||||
- Redirect or return nothing to unauthorized users before running restricted code
|
||||
|
||||
Reference: [Next.js Security Best Practices](https://nextjs.org/docs/app/building-your-application/authentication)
|
||||
@@ -0,0 +1,61 @@
|
||||
---
|
||||
title: Organize Code by Domain Using Vertical Slices
|
||||
impact: CRITICAL
|
||||
impactDescription: Dramatically improves discoverability and reduces cross-team conflicts
|
||||
tags: architecture, vertical-slices, ddd, organization
|
||||
---
|
||||
|
||||
## Organize Code by Domain Using Vertical Slices
|
||||
|
||||
**Impact: CRITICAL**
|
||||
|
||||
Our codebase is organized by domain, not by technical layer. The `packages/features` directory is the heart of this architectural approach. Each folder inside represents a complete vertical slice of the application, driven by the domain it touches.
|
||||
|
||||
**Incorrect (traditional layered architecture):**
|
||||
|
||||
```
|
||||
src/
|
||||
controllers/
|
||||
bookingController.ts
|
||||
availabilityController.ts
|
||||
services/
|
||||
bookingService.ts
|
||||
availabilityService.ts
|
||||
repositories/
|
||||
bookingRepository.ts
|
||||
availabilityRepository.ts
|
||||
```
|
||||
|
||||
This creates problems: changes to one feature require touching files scattered across multiple directories, it's hard to understand what a feature does because its code is fragmented, and teams step on each other's toes.
|
||||
|
||||
**Correct (vertical slice architecture):**
|
||||
|
||||
```
|
||||
packages/features/
|
||||
bookings/
|
||||
services/
|
||||
repositories/
|
||||
components/
|
||||
tests/
|
||||
availability/
|
||||
services/
|
||||
repositories/
|
||||
components/
|
||||
tests/
|
||||
```
|
||||
|
||||
Each feature folder is a self-contained vertical slice that includes:
|
||||
- Domain logic: Core business rules and entities specific to that feature
|
||||
- Application services: Use case orchestration for that domain
|
||||
- Repositories: Data access specific to that feature's needs
|
||||
- DTOs: Data transfer objects for crossing boundaries
|
||||
- UI components: Frontend components related to this feature
|
||||
- Tests: Unit, integration, and e2e tests for this feature
|
||||
|
||||
**Benefits:**
|
||||
- Everything related to a feature lives in one directory
|
||||
- You can understand the entire feature by exploring one directory
|
||||
- Teams can work on different features without conflicts
|
||||
- Features are loosely coupled and can evolve independently
|
||||
|
||||
Reference: [Cal.diy Engineering Blog](https://cal.com/blog/engineering-in-2026-and-beyond)
|
||||
@@ -0,0 +1,33 @@
|
||||
---
|
||||
title: CI Check Failure Handling
|
||||
impact: HIGH
|
||||
impactDescription: Misinterpreting CI failures wastes debugging time
|
||||
tags: ci, debugging, workflow
|
||||
---
|
||||
|
||||
# CI Check Failure Handling
|
||||
|
||||
## What to Focus On
|
||||
|
||||
When reviewing CI check failures in Cal.diy:
|
||||
|
||||
1. **E2E tests can be flaky** and may fail intermittently
|
||||
2. **Focus only on CI failures that are directly related to your code changes**
|
||||
3. Infrastructure-related failures (like dependency installation issues) can be disregarded if all code-specific checks pass
|
||||
|
||||
## Known CI Issues to Ignore
|
||||
|
||||
These errors are related to SAML database misconfiguration on CI and should be ignored:
|
||||
- "password authentication failed for user postgres"
|
||||
- "Invalid URL"
|
||||
|
||||
## E2E Tests Skipping
|
||||
|
||||
**E2E tests skipping is expected behavior:**
|
||||
- When E2E tests are skipped, it's because the `ready-for-e2e` label has not been added to the PR
|
||||
- The "required" check intentionally fails when E2E tests are skipped to prevent merging without E2E
|
||||
- Do not try to fix anything related to skipped E2E tests - this is completely expected and normal
|
||||
|
||||
## Before Blaming CI
|
||||
|
||||
Always run type checks locally using `yarn type-check:ci --force` before concluding that CI failures are unrelated to your changes. Even if errors appear in files you haven't directly modified, your changes might still be causing type issues through dependencies or type inference.
|
||||
@@ -0,0 +1,37 @@
|
||||
---
|
||||
title: Git and CI Workflow
|
||||
impact: HIGH
|
||||
impactDescription: Incorrect workflow causes wasted CI cycles and confusion
|
||||
tags: git, ci, workflow
|
||||
---
|
||||
|
||||
# Git and CI Workflow
|
||||
|
||||
## Push Before Checking CI
|
||||
|
||||
Always push committed changes to the remote repository before waiting for or checking CI status.
|
||||
|
||||
Waiting for CI checks on unpushed local commits is backwards - the CI runs on the remote repository state, not local commits.
|
||||
|
||||
**Proper sequence:**
|
||||
1. Commit locally
|
||||
2. Run local checks (`yarn type-check:ci --force`, `yarn biome check --write .`)
|
||||
3. Push to remote
|
||||
4. Monitor CI status
|
||||
|
||||
## Branch Operations
|
||||
|
||||
When asked to move changes to a different branch, use git commands to commit existing changes to the specified branch rather than redoing the work. This is more efficient and prevents duplication of effort.
|
||||
|
||||
## Never Force Push
|
||||
|
||||
**Never force push to main or production branches** - under any circumstances.
|
||||
|
||||
## Working with tRPC Changes
|
||||
|
||||
When making changes that affect tRPC components or after pulling updates that modify tRPC-related files:
|
||||
|
||||
1. First run `yarn prisma generate` to ensure all database types are up-to-date
|
||||
2. Then run `cd packages/trpc && yarn build` to rebuild the tRPC package
|
||||
|
||||
This sequence ensures that type definitions are properly generated before building.
|
||||
@@ -0,0 +1,43 @@
|
||||
---
|
||||
title: Type Check Before Tests
|
||||
impact: HIGH
|
||||
impactDescription: Type errors are often the root cause of test failures
|
||||
tags: ci, typescript, type-check, workflow
|
||||
---
|
||||
|
||||
# Type Check Before Tests
|
||||
|
||||
## Priority Order
|
||||
|
||||
When working on the Cal.diy repository, prioritize fixing type issues before addressing failing tests.
|
||||
|
||||
1. Run `yarn type-check:ci --force` first
|
||||
2. Fix all TypeScript errors
|
||||
3. Then run tests with `TZ=UTC yarn test`
|
||||
|
||||
## Why Type Check First
|
||||
|
||||
Type errors are often the root cause of test failures. Fixing types first:
|
||||
- Eliminates cascading failures
|
||||
- Ensures code compiles correctly
|
||||
- Catches issues that tests might miss
|
||||
|
||||
## Comparing Branches
|
||||
|
||||
Compare type check results between the main branch and your feature branch to confirm whether you've introduced new type errors:
|
||||
|
||||
```bash
|
||||
# On your branch
|
||||
yarn type-check:ci --force 2>&1 | tee /tmp/feature-types.log
|
||||
|
||||
# On main
|
||||
git checkout main
|
||||
yarn type-check:ci --force 2>&1 | tee /tmp/main-types.log
|
||||
|
||||
# Compare
|
||||
diff /tmp/main-types.log /tmp/feature-types.log
|
||||
```
|
||||
|
||||
## Missing Enum Errors
|
||||
|
||||
If you encounter errors related to missing enum values (like `CreationSource.WEBAPP`), running `yarn prisma generate` will typically resolve these issues by regenerating the TypeScript types from the schema.
|
||||
@@ -0,0 +1,45 @@
|
||||
---
|
||||
title: Hold Each Other Accountable for Quality
|
||||
impact: MEDIUM
|
||||
impactDescription: Builds collective ownership and prevents technical debt
|
||||
tags: culture, accountability, quality, teamwork
|
||||
---
|
||||
|
||||
## Hold Each Other Accountable for Quality
|
||||
|
||||
**Impact: MEDIUM**
|
||||
|
||||
We hold each other accountable for quality. Cutting corners might feel faster in the moment, but it creates problems that slow everyone down later. When you see a teammate about to merge a PR with obvious issues, speak up.
|
||||
|
||||
**This isn't about being difficult or slowing people down.** It's about collective ownership of our codebase and our reputation. Every shortcut one person takes becomes everyone's problem. Every corner cut today means more debugging sessions, more hotfixes, and more frustrated customers tomorrow.
|
||||
|
||||
**Make it normal to challenge poor decisions, respectfully:**
|
||||
|
||||
```
|
||||
// When someone says:
|
||||
"Let's just hard-code this for now"
|
||||
|
||||
// The expected response:
|
||||
"What would it take to do it the proper way the first time?"
|
||||
```
|
||||
|
||||
```
|
||||
// When someone wants to commit untested code:
|
||||
"Can we add tests for this before merging? I can help if needed."
|
||||
```
|
||||
|
||||
```
|
||||
// When someone suggests copy-paste instead of abstraction:
|
||||
"This looks like it could be a shared utility. Should we extract it?"
|
||||
```
|
||||
|
||||
**We're building something that needs to almost never fail.** That level of reliability doesn't happen by accident. It happens when every engineer feels responsible for quality - not just their own code but the entire system. We succeed as a team or we fail as a team.
|
||||
|
||||
**Key behaviors:**
|
||||
- Push back when you see shortcuts being taken
|
||||
- Offer to help when suggesting improvements
|
||||
- Accept feedback gracefully when others challenge your decisions
|
||||
- Focus on the code, not the person
|
||||
- Remember that quality standards protect everyone
|
||||
|
||||
Reference: [Cal.diy Engineering Blog](https://cal.com/blog/engineering-in-2026-and-beyond)
|
||||
@@ -0,0 +1,75 @@
|
||||
---
|
||||
title: Leverage AI for Boilerplate and Testing
|
||||
impact: MEDIUM
|
||||
impactDescription: Accelerates development while maintaining quality
|
||||
tags: culture, ai, automation, testing
|
||||
---
|
||||
|
||||
## Leverage AI for Boilerplate and Testing
|
||||
|
||||
**Impact: MEDIUM**
|
||||
|
||||
Generate 80% of boilerplate and non-critical code using AI, allowing us to focus solely on complex business logic and critical architectures.
|
||||
|
||||
**Where AI excels:**
|
||||
- Generating boilerplate code (DTOs, basic CRUD operations)
|
||||
- Building comprehensive test suites
|
||||
- Creating documentation
|
||||
- Repetitive refactoring tasks
|
||||
- Code review assistance
|
||||
|
||||
**Where humans must focus:**
|
||||
- Complex business logic
|
||||
- Critical architectural decisions
|
||||
- Security-sensitive code
|
||||
- Performance-critical algorithms
|
||||
- Domain-specific edge cases
|
||||
|
||||
**Example - AI-assisted test generation:**
|
||||
|
||||
```typescript
|
||||
// Human writes the function
|
||||
export function calculateOverlap(slot: TimeSlot, busy: BusyTime): boolean {
|
||||
return slot.start < busy.end && slot.end > busy.start;
|
||||
}
|
||||
|
||||
// AI generates comprehensive tests
|
||||
describe("calculateOverlap", () => {
|
||||
it("returns true when slot starts during busy period", () => {
|
||||
// AI-generated test case
|
||||
});
|
||||
|
||||
it("returns true when slot ends during busy period", () => {
|
||||
// AI-generated test case
|
||||
});
|
||||
|
||||
it("returns false when slot is completely before busy period", () => {
|
||||
// AI-generated test case
|
||||
});
|
||||
|
||||
it("returns false when slot is completely after busy period", () => {
|
||||
// AI-generated test case
|
||||
});
|
||||
|
||||
it("returns true when slot completely contains busy period", () => {
|
||||
// AI-generated test case
|
||||
});
|
||||
|
||||
it("returns true when busy period completely contains slot", () => {
|
||||
// AI-generated test case
|
||||
});
|
||||
|
||||
// AI identifies edge cases humans might miss
|
||||
it("handles exact boundary matches correctly", () => {
|
||||
// AI-generated edge case
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
**Our CI is the final boss:**
|
||||
- Everything in our standards document is checked before code is merged in PRs
|
||||
- No surprises make it into main
|
||||
- Checks are fast and useful
|
||||
- AI helps ensure comprehensive coverage
|
||||
|
||||
Reference: [Cal.diy Engineering Blog](https://cal.com/blog/engineering-in-2026-and-beyond)
|
||||
@@ -0,0 +1,96 @@
|
||||
---
|
||||
title: Use DTOs at Every Architectural Boundary
|
||||
impact: CRITICAL
|
||||
impactDescription: Prevents technology coupling and security risks
|
||||
tags: data, dto, boundaries, security, types
|
||||
---
|
||||
|
||||
## Use DTOs at Every Architectural Boundary
|
||||
|
||||
**Impact: CRITICAL**
|
||||
|
||||
Database types should not leak to the frontend. This has become a popular shortcut in our tech stack, but it's a code smell that creates multiple problems.
|
||||
|
||||
**Problems with leaking database types:**
|
||||
- Technology coupling (Prisma types end up in React components)
|
||||
- Security risks (accidental leakage of sensitive fields)
|
||||
- Fragile contracts between server and client
|
||||
- Inability to evolve the database schema independently
|
||||
|
||||
**Incorrect (database types leaking):**
|
||||
|
||||
```typescript
|
||||
// API route returning Prisma types directly
|
||||
import type { User } from "@prisma/client";
|
||||
|
||||
export async function GET(): Promise<User> {
|
||||
const user = await prisma.user.findFirst();
|
||||
return user; // Leaks all database fields including sensitive ones
|
||||
}
|
||||
|
||||
// Frontend using Prisma types
|
||||
import type { User } from "@prisma/client";
|
||||
|
||||
function UserProfile({ user }: { user: User }) {
|
||||
// Component now coupled to database schema
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (explicit DTOs):**
|
||||
|
||||
```typescript
|
||||
// Define explicit DTOs
|
||||
interface UserDTO {
|
||||
id: number;
|
||||
name: string;
|
||||
email: string;
|
||||
// Only fields needed by the client
|
||||
}
|
||||
|
||||
// API route transforms to DTO
|
||||
export async function GET(): Promise<UserDTO> {
|
||||
const user = await userRepository.findById(id);
|
||||
return UserResponseSchema.parse(user); // Validate with Zod
|
||||
}
|
||||
|
||||
// Frontend uses DTO
|
||||
function UserProfile({ user }: { user: UserDTO }) {
|
||||
// Component decoupled from database
|
||||
}
|
||||
```
|
||||
|
||||
**The standard:**
|
||||
1. **Data layer → Application layer → API**: Transform database models into application-layer DTOs, then transform application DTOs into API-specific DTOs
|
||||
2. **API → Application layer → Data layer**: Transform API DTOs through application layer and into data-specific DTOs
|
||||
3. All DTO conversions through Zod to ensure all data is validated before sending to user
|
||||
|
||||
### DTO Location and Naming
|
||||
|
||||
**Location**: All DTOs go in `packages/lib/dto/`
|
||||
|
||||
**Naming conventions**:
|
||||
- Base entity: `{Entity}Dto` (e.g., `BookingDto`)
|
||||
- With relations: `{Entity}With{Relations}Dto` (e.g., `BookingWithAttendeesDto`)
|
||||
- For specific projections: `{Entity}For{Purpose}Dto` (e.g., `BookingForConfirmationDto`)
|
||||
- Avoid: `{Entity}Dto2`, `{Entity}DtoForHandler`, or other use-case-specific names
|
||||
|
||||
**Enum/union pattern** - use string literal unions to stay ORM-agnostic:
|
||||
|
||||
```typescript
|
||||
// Good - ORM-agnostic string literal union
|
||||
export type BookingStatusDto = "CANCELLED" | "ACCEPTED" | "REJECTED" | "PENDING";
|
||||
|
||||
// Bad - importing Prisma enum
|
||||
import { BookingStatus } from "@calcom/prisma/client";
|
||||
```
|
||||
|
||||
**Type safety** - never use `as any` in DTO mapping functions. If types don't align, fix the mapping explicitly.
|
||||
|
||||
### Prisma Boundaries
|
||||
|
||||
- **Allowed**: `packages/prisma`, repository implementations (`packages/features/**/repositories/*Repository.ts`), and low-level data access infrastructure.
|
||||
- **Not allowed**: `packages/features/**` business logic (non-repository), `packages/trpc/**` handlers, `apps/web/**`, `apps/api/v2/**` services/controllers, and workflow/webhook/service layers.
|
||||
|
||||
Yes, this requires more code. Yes, it's worth it. Explicit boundaries prevent the architectural erosion that creates long-term maintenance nightmares.
|
||||
|
||||
Reference: [Cal.diy Engineering Blog](https://cal.com/blog/engineering-in-2026-and-beyond)
|
||||
@@ -0,0 +1,49 @@
|
||||
---
|
||||
title: Prefer Select Over Include in Prisma Queries
|
||||
impact: HIGH
|
||||
impactDescription: Reduces data transfer and improves query performance
|
||||
tags: prisma, database, performance, security
|
||||
---
|
||||
|
||||
## Prefer Select Over Include in Prisma Queries
|
||||
|
||||
**Impact: HIGH (Reduces data transfer and improves query performance)**
|
||||
|
||||
Using `select` instead of `include` in Prisma queries fetches only the fields you need, improving performance and preventing accidental exposure of sensitive data.
|
||||
|
||||
**Incorrect (using include fetches all fields):**
|
||||
|
||||
```typescript
|
||||
const booking = await prisma.booking.findFirst({
|
||||
include: {
|
||||
user: true, // This gets ALL user fields including sensitive ones
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
**Correct (using select for specific fields):**
|
||||
|
||||
```typescript
|
||||
const booking = await prisma.booking.findFirst({
|
||||
select: {
|
||||
id: true,
|
||||
title: true,
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- **Performance**: Smaller payloads, faster queries
|
||||
- **Security**: Prevents accidental exposure of sensitive fields (e.g., `credential.key`)
|
||||
- **Clarity**: Makes data requirements explicit
|
||||
|
||||
**Exception:** Use `include` only when you genuinely need all fields from a relation, which is rare.
|
||||
|
||||
Reference: [Cal.diy Engineering Standards](https://cal.com/blog/engineering-in-2026-and-beyond)
|
||||
@@ -0,0 +1,40 @@
|
||||
---
|
||||
title: Feature Flag Seeding
|
||||
impact: MEDIUM
|
||||
impactDescription: Proper feature flag setup enables controlled rollouts
|
||||
tags: prisma, feature-flags, migrations
|
||||
---
|
||||
|
||||
# Feature Flag Seeding
|
||||
|
||||
## Creating Feature Flag Migrations
|
||||
|
||||
To seed new feature flags in Cal.diy, create a Prisma migration:
|
||||
|
||||
```bash
|
||||
yarn prisma migrate dev --create-only --name seed_[feature_name]_feature
|
||||
```
|
||||
|
||||
## Migration File Location
|
||||
|
||||
The migration file should be placed in `packages/prisma/migrations/` with a timestamp prefix format:
|
||||
|
||||
```
|
||||
20250724210733_seed_calendar_cache_sql_features/migration.sql
|
||||
```
|
||||
|
||||
## SQL Structure
|
||||
|
||||
Follow the pattern from existing feature seeding migrations like:
|
||||
`packages/prisma/migrations/20241216000000_add_calendar_cache_serve/migration.sql`
|
||||
|
||||
```sql
|
||||
INSERT INTO "Feature" ("slug", "enabled", "type", "description", "createdAt", "updatedAt")
|
||||
VALUES
|
||||
('your-feature-slug', false, 'OPERATIONAL', 'Description of feature', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
||||
ON CONFLICT ("slug") DO NOTHING;
|
||||
```
|
||||
|
||||
The migration should INSERT the new features into the `Feature` table with:
|
||||
- Appropriate type (like `OPERATIONAL`)
|
||||
- Default `enabled` status for manual team enablement
|
||||
@@ -0,0 +1,52 @@
|
||||
---
|
||||
title: Prisma Schema and Migrations
|
||||
impact: HIGH
|
||||
impactDescription: Schema changes affect all downstream code and deployments
|
||||
tags: prisma, database, migrations, schema
|
||||
---
|
||||
|
||||
# Prisma Schema and Migrations
|
||||
|
||||
## After Schema Changes
|
||||
|
||||
After making changes to the Prisma schema in Cal.diy and creating migrations, you need to run:
|
||||
|
||||
```bash
|
||||
yarn prisma generate
|
||||
```
|
||||
|
||||
This updates the TypeScript types. This is especially important:
|
||||
- When switching Node.js versions
|
||||
- After adding new fields to models
|
||||
- After pulling changes that include Prisma schema updates
|
||||
|
||||
## Creating Migrations
|
||||
|
||||
```bash
|
||||
# Development migration
|
||||
npx prisma migrate dev --name migration_name
|
||||
|
||||
# Production deployment
|
||||
yarn workspace @calcom/prisma db-deploy
|
||||
```
|
||||
|
||||
## Timestamp Fields
|
||||
|
||||
When adding timestamp fields like `createdAt` and `updatedAt`:
|
||||
- Do not set default values if you want existing records to have null values
|
||||
- Only new records should get timestamps automatically
|
||||
- For `updatedAt` fields, ensure they're updated when records are modified
|
||||
|
||||
## Squash Migrations
|
||||
|
||||
Whenever you change the schema.prisma file, remember to always consolidate migrations by squashing them as declared in the [Prisma docs](https://www.prisma.io/docs/orm/prisma-migrate/workflows/squashing-migrations).
|
||||
|
||||
This helps maintain a clean migration history and prevents accumulation of multiple migration files.
|
||||
|
||||
## Enum Generator Errors
|
||||
|
||||
If you encounter enum generator errors during the Prisma generate step (like "Cannot find module './enum-generator.ts'"), run `yarn install` first before trying to generate.
|
||||
|
||||
## Cache-Related Features
|
||||
|
||||
When implementing cache-related features that require timestamp tracking, always update the database schema first before modifying application code that references those fields.
|
||||
@@ -0,0 +1,200 @@
|
||||
---
|
||||
title: Repository Method Naming Conventions
|
||||
impact: HIGH
|
||||
impactDescription: Improves code discoverability and reusability
|
||||
tags: data, repository, naming, conventions, methods
|
||||
---
|
||||
|
||||
## Repository Method Naming Conventions
|
||||
|
||||
**Impact: HIGH**
|
||||
|
||||
Repository methods should follow consistent naming conventions to improve discoverability and promote code reuse across different features.
|
||||
|
||||
### Rule 1: Don't include the repository's entity name in method names
|
||||
|
||||
Method names should be concise and avoid redundancy since the repository class name already indicates the entity type.
|
||||
|
||||
```typescript
|
||||
// Good - Concise method names
|
||||
class BookingRepository {
|
||||
findById(id: string) { ... }
|
||||
findByUserId(userId: string) { ... }
|
||||
create(data: BookingCreateInput) { ... }
|
||||
delete(id: string) { ... }
|
||||
}
|
||||
|
||||
// Bad - Redundant entity name in methods
|
||||
class BookingRepository {
|
||||
findBookingById(id: string) { ... }
|
||||
findBookingByUserId(userId: string) { ... }
|
||||
createBooking(data: BookingCreateInput) { ... }
|
||||
deleteBooking(id: string) { ... }
|
||||
}
|
||||
```
|
||||
|
||||
### Rule 2: Use `include` or similar keywords for methods that fetch relational data
|
||||
|
||||
When a method retrieves additional related entities, make this explicit in the method name using keywords like `include`, `with`, or `andRelations`.
|
||||
|
||||
```typescript
|
||||
// Good - Clear indication of included relations
|
||||
class EventTypeRepository {
|
||||
findById(id: string) {
|
||||
return prisma.eventType.findUnique({
|
||||
where: { id },
|
||||
});
|
||||
}
|
||||
|
||||
findByIdIncludeHosts(id: string) {
|
||||
return prisma.eventType.findUnique({
|
||||
where: { id },
|
||||
include: { hosts: true },
|
||||
});
|
||||
}
|
||||
|
||||
findByIdIncludeHostsAndSchedule(id: string) {
|
||||
return prisma.eventType.findUnique({
|
||||
where: { id },
|
||||
include: { hosts: true, schedule: true },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Bad - Unclear what data is included
|
||||
class EventTypeRepository {
|
||||
findById(id: string) {
|
||||
return prisma.eventType.findUnique({
|
||||
where: { id },
|
||||
include: { hosts: true, schedule: true },
|
||||
});
|
||||
}
|
||||
|
||||
findByIdForReporting(id: string) {
|
||||
return prisma.eventType.findUnique({
|
||||
where: { id },
|
||||
include: { hosts: true },
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Rule 3: Keep methods generic and reusable - avoid use-case-specific names
|
||||
|
||||
Repository methods should be general-purpose and describe what data they return, not how or where it's used. This promotes code reuse across different features.
|
||||
|
||||
```typescript
|
||||
// Good - Generic, reusable methods
|
||||
class BookingRepository {
|
||||
findByUserIdIncludeAttendees(userId: string) {
|
||||
return prisma.booking.findMany({
|
||||
where: { userId },
|
||||
include: { attendees: true },
|
||||
});
|
||||
}
|
||||
|
||||
findByDateRangeIncludeEventType(startDate: Date, endDate: Date) {
|
||||
return prisma.booking.findMany({
|
||||
where: {
|
||||
startTime: { gte: startDate },
|
||||
endTime: { lte: endDate },
|
||||
},
|
||||
include: { eventType: true },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Bad - Use-case-specific method names
|
||||
class BookingRepository {
|
||||
findBookingsForReporting(userId: string) {
|
||||
return prisma.booking.findMany({
|
||||
where: { userId },
|
||||
include: { attendees: true },
|
||||
});
|
||||
}
|
||||
|
||||
findBookingsForDashboard(startDate: Date, endDate: Date) {
|
||||
return prisma.booking.findMany({
|
||||
where: {
|
||||
startTime: { gte: startDate },
|
||||
endTime: { lte: endDate },
|
||||
},
|
||||
include: { eventType: true },
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Rule 4: No business logic in repositories
|
||||
|
||||
Repositories should only handle data access. Business logic, validations, and complex transformations belong in the Service layer.
|
||||
|
||||
```typescript
|
||||
// Good - Repository only handles data access
|
||||
class BookingRepository {
|
||||
findByIdIncludeAttendees(id: string) {
|
||||
return prisma.booking.findUnique({
|
||||
where: { id },
|
||||
include: { attendees: true },
|
||||
});
|
||||
}
|
||||
|
||||
updateStatus(id: string, status: BookingStatus) {
|
||||
return prisma.booking.update({
|
||||
where: { id },
|
||||
data: { status },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class BookingService {
|
||||
async confirmBooking(bookingId: string) {
|
||||
const booking = await this.bookingRepository.findByIdIncludeAttendees(bookingId);
|
||||
|
||||
if (!booking) {
|
||||
throw new Error("Booking not found");
|
||||
}
|
||||
|
||||
if (booking.status !== "PENDING") {
|
||||
throw new Error("Only pending bookings can be confirmed");
|
||||
}
|
||||
|
||||
await this.emailService.sendConfirmationToAttendees(booking.attendees);
|
||||
return this.bookingRepository.updateStatus(bookingId, "CONFIRMED");
|
||||
}
|
||||
}
|
||||
|
||||
// Bad - Business logic in repository
|
||||
class BookingRepository {
|
||||
async confirmBooking(bookingId: string) {
|
||||
const booking = await prisma.booking.findUnique({
|
||||
where: { id: bookingId },
|
||||
include: { attendees: true },
|
||||
});
|
||||
|
||||
if (!booking) {
|
||||
throw new Error("Booking not found");
|
||||
}
|
||||
|
||||
if (booking.status !== "PENDING") {
|
||||
throw new Error("Only pending bookings can be confirmed");
|
||||
}
|
||||
|
||||
await sendEmailToAttendees(booking.attendees);
|
||||
|
||||
return prisma.booking.update({
|
||||
where: { id: bookingId },
|
||||
data: { status: "CONFIRMED" },
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Summary
|
||||
|
||||
- Method names should be concise: `findById` not `findBookingById`
|
||||
- Use `include`/`with` keywords when fetching relations: `findByIdIncludeHosts`
|
||||
- Keep methods generic and reusable: `findByUserIdIncludeAttendees` not `findBookingsForReporting`
|
||||
- No business logic in repositories - that belongs in Services
|
||||
|
||||
Reference: [Cal.diy Engineering Blog](https://cal.com/blog/engineering-in-2026-and-beyond)
|
||||
@@ -0,0 +1,65 @@
|
||||
---
|
||||
title: Isolate Technology Choices Behind Repositories
|
||||
impact: CRITICAL
|
||||
impactDescription: Enables technology changes without codebase-wide refactors
|
||||
tags: data, repository, prisma, orm, isolation
|
||||
---
|
||||
|
||||
## Isolate Technology Choices Behind Repositories
|
||||
|
||||
**Impact: CRITICAL**
|
||||
|
||||
Technology choices must not seep through the application. The Prisma problem illustrates this perfectly: we currently have references to Prisma scattered across hundreds of files. This creates massive coupling and makes technology changes prohibitively expensive.
|
||||
|
||||
**Incorrect (Prisma leaking throughout codebase):**
|
||||
|
||||
```typescript
|
||||
// In a service file
|
||||
import { prisma } from "@calcom/prisma";
|
||||
|
||||
async function getBooking(id: number) {
|
||||
// Direct Prisma usage in service
|
||||
return prisma.booking.findFirst({
|
||||
where: { id },
|
||||
include: { user: true }
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (Repository abstraction):**
|
||||
|
||||
```typescript
|
||||
// In repository file
|
||||
import { prisma } from "@calcom/prisma";
|
||||
|
||||
export class BookingRepository {
|
||||
async findById(id: number): Promise<BookingDTO | null> {
|
||||
const booking = await prisma.booking.findFirst({
|
||||
where: { id },
|
||||
select: { id: true, title: true, userId: true }
|
||||
});
|
||||
return booking ? this.toDTO(booking) : null;
|
||||
}
|
||||
}
|
||||
|
||||
// In service file - no Prisma knowledge
|
||||
import { BookingRepository } from "./repositories/BookingRepository";
|
||||
|
||||
async function getBooking(id: number) {
|
||||
return this.bookingRepository.findById(id);
|
||||
}
|
||||
```
|
||||
|
||||
**The standard:**
|
||||
- All database access must go through Repository classes
|
||||
- Repositories are the only code that knows about Prisma (or any other ORM)
|
||||
- No business logic should be in repositories
|
||||
- Repositories are injected via Dependency Injection containers
|
||||
|
||||
**Benefits:**
|
||||
If we ever switch from Prisma to Drizzle or another ORM, the only changes required are:
|
||||
- Repository implementations
|
||||
- DI container wiring for new repositories
|
||||
- Nothing else in the codebase should care or change
|
||||
|
||||
Reference: [Cal.diy Engineering Blog](https://cal.com/blog/engineering-in-2026-and-beyond)
|
||||
@@ -0,0 +1,31 @@
|
||||
---
|
||||
title: App Store Integration Patterns
|
||||
impact: MEDIUM
|
||||
impactDescription: App store integrations require specific patterns for generated files
|
||||
tags: app-store, integrations, generated-files
|
||||
---
|
||||
|
||||
# App Store Integration Patterns
|
||||
|
||||
## Generated Files
|
||||
|
||||
The Cal.diy repository uses generated files (`*.generated.ts`) for app-store integrations. These files are created by the app-store-cli tool.
|
||||
|
||||
**Do not manually modify** `*.generated.ts` files. If you need structural changes to how integrations are imported or used, update the CLI code that generates these files.
|
||||
|
||||
## Import Patterns
|
||||
|
||||
Recent changes have moved from dynamic imports to static map-based imports for better performance. When working with browser components in the app-store, static imports should be used rather than dynamic imports.
|
||||
|
||||
### Generated File Types
|
||||
|
||||
When modifying the app-store-cli `build.ts` file, ensure it correctly handles all types:
|
||||
|
||||
1. **Regular service files** (`calendar.services.generated.ts`, `crm.services.generated.ts`, etc.) - need default imports
|
||||
2. **Browser component files** (`apps.browser-addon.generated.tsx`, etc.) - may require dynamic imports with Next.js
|
||||
|
||||
The `lazyImport` parameter in `getExportedObject()` determines whether to use dynamic imports (for browser components) or static imports (for server-side services).
|
||||
|
||||
## Calendar Cache Features
|
||||
|
||||
The calendar cache system follows specific patterns in `packages/features/calendar-cache-sql`. When implementing provider-specific calendar cache services (like for Outlook/Office365), the provider-specific code should be placed in the corresponding provider directory (e.g., `packages/app-store/office365calendar`).
|
||||
@@ -0,0 +1,280 @@
|
||||
---
|
||||
title: Use Dependency Injection for Loose Coupling
|
||||
impact: HIGH
|
||||
impactDescription: Enables build-time safety, testability, and maintainability
|
||||
tags: patterns, dependency-injection, di, ioctopus, moduleloader, testing, coupling
|
||||
---
|
||||
|
||||
## Use Dependency Injection for Loose Coupling
|
||||
|
||||
**Impact: HIGH**
|
||||
|
||||
Dependency Injection enables loose coupling, facilitates testing, and isolates concerns. Dependencies should be injected via DI containers rather than instantiated directly within classes.
|
||||
|
||||
**Incorrect (tight coupling with direct instantiation):**
|
||||
|
||||
```typescript
|
||||
class BookingService {
|
||||
private repository = new BookingRepository();
|
||||
private emailService = new EmailService();
|
||||
private calendarService = new GoogleCalendarService();
|
||||
|
||||
async createBooking(data: CreateBookingDTO) {
|
||||
const booking = await this.repository.create(data);
|
||||
await this.emailService.sendConfirmation(booking);
|
||||
await this.calendarService.createEvent(booking);
|
||||
return booking;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (dependency injection):**
|
||||
|
||||
```typescript
|
||||
class BookingService {
|
||||
constructor(
|
||||
private readonly repository: BookingRepository,
|
||||
private readonly emailService: EmailService,
|
||||
private readonly calendarService: CalendarService,
|
||||
) {}
|
||||
|
||||
async createBooking(data: CreateBookingDTO) {
|
||||
const booking = await this.repository.create(data);
|
||||
await this.emailService.sendConfirmation(booking);
|
||||
await this.calendarService.createEvent(booking);
|
||||
return booking;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Required patterns:**
|
||||
- **Application Services**: Orchestrate use cases, coordinate between domain services and repositories
|
||||
- **Domain Services**: Contain business logic that doesn't naturally belong to a single entity
|
||||
- **Repositories**: Abstract data access, isolate technology choices
|
||||
- **Caching Proxies**: Wrap repositories or services to add caching behavior transparently
|
||||
- **Decorators**: Add cross-cutting concerns (logging, metrics) without polluting domain logic
|
||||
|
||||
## Cal.diy's Type-Safe DI with moduleLoader
|
||||
|
||||
We use `@evyweb/ioctopus` to manage service and repository dependencies. The **moduleLoader pattern** provides type-safe dependency injection, ensuring that if a service adds a new dependency, TypeScript will catch missing dependencies at build time rather than runtime.
|
||||
|
||||
### Core Concepts
|
||||
|
||||
**Tokens**: Unique symbols that identify each service or repository in the DI container. Every injectable class needs a corresponding token. Tokens should be defined in a `tokens.ts` file within the feature's `di/` directory.
|
||||
|
||||
**Modules** (`.module.ts` files): Define how classes are instantiated and what dependencies they require using the `bindModuleToClassOnToken` function. Each module exports a `moduleLoader` object that knows how to load itself and its dependencies.
|
||||
|
||||
**Containers** (`.container.ts` files): Create a container instance and expose getter functions that consumers use to obtain service instances. Containers use the moduleLoader to automatically load all required dependencies.
|
||||
|
||||
**`bindModuleToClassOnToken`**: A type-safe function that binds a class to a token and declares its dependencies. TypeScript ensures the declared dependencies match what the class constructor expects.
|
||||
|
||||
### How It Works
|
||||
|
||||
**Step 1: Create tokens in the feature's di directory**
|
||||
|
||||
```typescript
|
||||
// packages/features/myfeature/di/tokens.ts
|
||||
export const MY_FEATURE_DI_TOKENS = {
|
||||
MY_SERVICE: Symbol("MyService"),
|
||||
MY_SERVICE_MODULE: Symbol("MyServiceModule"),
|
||||
};
|
||||
```
|
||||
|
||||
Then import these tokens in the central tokens file:
|
||||
|
||||
```typescript
|
||||
// packages/features/di/tokens.ts
|
||||
import { MY_FEATURE_DI_TOKENS } from "@calcom/features/myfeature/di/tokens";
|
||||
|
||||
export const DI_TOKENS = {
|
||||
// ...existing tokens
|
||||
...MY_FEATURE_DI_TOKENS,
|
||||
};
|
||||
```
|
||||
|
||||
**Step 2: Define the service class with constructor injection**
|
||||
|
||||
For services with multiple dependencies, use a dependencies interface:
|
||||
|
||||
```typescript
|
||||
// packages/features/myfeature/services/MyService.ts
|
||||
export interface IMyServiceDeps {
|
||||
bookingRepo: BookingRepository;
|
||||
userRepo: UserRepository;
|
||||
}
|
||||
|
||||
export class MyService {
|
||||
constructor(private deps: IMyServiceDeps) {}
|
||||
|
||||
async doSomething() {
|
||||
const bookings = await this.deps.bookingRepo.findMany({...});
|
||||
const user = await this.deps.userRepo.findById({...});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
For services/repositories with a single dependency, pass it directly:
|
||||
|
||||
```typescript
|
||||
// packages/features/myfeature/repositories/MyRepository.ts
|
||||
export class MyRepository {
|
||||
constructor(private prismaClient: PrismaClient) {}
|
||||
|
||||
async findById(id: string) {
|
||||
return this.prismaClient.myModel.findUnique({ where: { id } });
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Step 3: Create a module file with moduleLoader**
|
||||
|
||||
```typescript
|
||||
// packages/features/myfeature/di/MyService.module.ts
|
||||
import { bindModuleToClassOnToken, createModule, type ModuleLoader } from "@calcom/features/di/di";
|
||||
import { MyService } from "@calcom/features/myfeature/services/MyService";
|
||||
|
||||
import { moduleLoader as bookingRepositoryModuleLoader } from "./BookingRepository.module";
|
||||
import { moduleLoader as userRepositoryModuleLoader } from "./UserRepository.module";
|
||||
import { MY_FEATURE_DI_TOKENS } from "./tokens";
|
||||
|
||||
const thisModule = createModule();
|
||||
const token = MY_FEATURE_DI_TOKENS.MY_SERVICE;
|
||||
const moduleToken = MY_FEATURE_DI_TOKENS.MY_SERVICE_MODULE;
|
||||
|
||||
const loadModule = bindModuleToClassOnToken({
|
||||
module: thisModule,
|
||||
moduleToken,
|
||||
token,
|
||||
classs: MyService,
|
||||
depsMap: {
|
||||
bookingRepo: bookingRepositoryModuleLoader,
|
||||
userRepo: userRepositoryModuleLoader,
|
||||
},
|
||||
});
|
||||
|
||||
export const moduleLoader: ModuleLoader = {
|
||||
token,
|
||||
loadModule,
|
||||
};
|
||||
|
||||
export type { MyService };
|
||||
```
|
||||
|
||||
For a single dependency, use `dep` instead of `depsMap`:
|
||||
|
||||
```typescript
|
||||
// packages/features/myfeature/di/MyRepository.module.ts
|
||||
const loadModule = bindModuleToClassOnToken({
|
||||
module: thisModule,
|
||||
moduleToken,
|
||||
token,
|
||||
classs: MyRepository,
|
||||
dep: prismaModuleLoader,
|
||||
});
|
||||
```
|
||||
|
||||
**Step 4: Create a container that uses the moduleLoader**
|
||||
|
||||
```typescript
|
||||
// packages/features/myfeature/di/MyService.container.ts
|
||||
import { createContainer } from "@calcom/features/di/di";
|
||||
import { type MyService, moduleLoader as myServiceModuleLoader } from "./MyService.module";
|
||||
|
||||
const myServiceContainer = createContainer();
|
||||
|
||||
export function getMyService(): MyService {
|
||||
myServiceModuleLoader.loadModule(myServiceContainer);
|
||||
return myServiceContainer.get<MyService>(myServiceModuleLoader.token);
|
||||
}
|
||||
```
|
||||
|
||||
**Step 5: Use the service via the container's getter function**
|
||||
|
||||
```typescript
|
||||
import { getMyService } from "@calcom/features/myfeature/di/MyService.container";
|
||||
|
||||
const myService = getMyService();
|
||||
await myService.doSomething();
|
||||
```
|
||||
|
||||
## Common Mistakes to Avoid
|
||||
|
||||
**Mistake 1: Creating a repository or service class with all static methods**
|
||||
|
||||
```typescript
|
||||
// Bad - Static methods bypass DI
|
||||
export class BookingRepository {
|
||||
static async findById(id: string) {
|
||||
return prisma.booking.findUnique({ where: { id } });
|
||||
}
|
||||
}
|
||||
|
||||
// Good - Instance methods with constructor injection
|
||||
export class BookingRepository {
|
||||
constructor(private prismaClient: PrismaClient) {}
|
||||
|
||||
async findById(id: string) {
|
||||
return this.prismaClient.booking.findUnique({ where: { id } });
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Mistake 2: Manually instantiating a class instead of using the DI container**
|
||||
|
||||
```typescript
|
||||
// Bad - Manual instantiation bypasses DI
|
||||
const bookingRepo = new BookingRepository(prisma);
|
||||
const myService = new MyService({ bookingRepo });
|
||||
|
||||
// Good - Use the DI container's getter function
|
||||
import { getMyService } from "@calcom/features/myfeature/di/MyService.container";
|
||||
const myService = getMyService();
|
||||
```
|
||||
|
||||
**Mistake 3: Importing Prisma directly in a service instead of using repository injection**
|
||||
|
||||
```typescript
|
||||
// Bad - Service imports Prisma directly
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
export class MyService {
|
||||
async doSomething() {
|
||||
const bookings = await prisma.booking.findMany({...});
|
||||
}
|
||||
}
|
||||
|
||||
// Good - Service depends on repository via DI
|
||||
export class MyService {
|
||||
constructor(private deps: IMyServiceDeps) {}
|
||||
|
||||
async doSomething() {
|
||||
const bookings = await this.deps.bookingRepo.findMany({...});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Mistake 4: Manual module loading in containers (not type-safe)**
|
||||
|
||||
```typescript
|
||||
// Bad - Manual module loading is not type-safe
|
||||
const container = createContainer();
|
||||
container.load(DI_TOKENS.PRISMA_MODULE, prismaModule);
|
||||
container.load(DI_TOKENS.BOOKING_REPOSITORY_MODULE, bookingRepositoryModule);
|
||||
|
||||
// Good - Use moduleLoader for type-safe dependency loading
|
||||
export function getMyService(): MyService {
|
||||
myServiceModuleLoader.loadModule(container);
|
||||
return container.get<MyService>(myServiceModuleLoader.token);
|
||||
}
|
||||
```
|
||||
|
||||
## Why Use DI?
|
||||
|
||||
- **Build-time safety**: TypeScript catches missing dependencies before runtime
|
||||
- **Testability**: Dependencies can be easily mocked in tests
|
||||
- **Consistency**: All instances are created the same way with proper dependencies
|
||||
- **Maintainability**: Changing a dependency only requires updating the module binding
|
||||
- **Automatic dependency resolution**: The moduleLoader automatically loads all dependencies recursively
|
||||
- **Self-documenting**: Each module declares its own dependencies explicitly
|
||||
|
||||
Reference: [Cal.diy Engineering Blog](https://cal.com/blog/engineering-in-2026-and-beyond)
|
||||
@@ -0,0 +1,87 @@
|
||||
---
|
||||
title: Use Factory Pattern to Push Conditionals to Entry Points
|
||||
impact: HIGH
|
||||
impactDescription: Keeps services focused and prevents complexity accumulation
|
||||
tags: patterns, factory, conditionals, single-responsibility
|
||||
---
|
||||
|
||||
## Use Factory Pattern to Push Conditionals to Entry Points
|
||||
|
||||
**Impact: HIGH**
|
||||
|
||||
If statements belong at the entry point, not scattered throughout your services. This is one of the most important architectural principles for maintaining clean, focused code that doesn't spiral into unmaintainable complexity.
|
||||
|
||||
**The problem with scattered conditionals:**
|
||||
A service is written for a clear, specific purpose. Then a new product requirement arrives, and someone adds an if statement. A few years later, that service is littered with conditional checks. The service becomes:
|
||||
- Complicated and hard to read
|
||||
- Difficult to understand and reason about
|
||||
- More susceptible to bugs
|
||||
- Violating single responsibility
|
||||
- Nearly impossible to test thoroughly
|
||||
|
||||
**Incorrect (conditionals scattered in service):**
|
||||
|
||||
```typescript
|
||||
class BillingService {
|
||||
async processPayment(entityId: number, entityType: string) {
|
||||
if (entityType === "organization") {
|
||||
// Organization-specific logic
|
||||
const org = await this.getOrganization(entityId);
|
||||
if (org.billingPlan === "enterprise") {
|
||||
// More nested conditionals...
|
||||
}
|
||||
} else if (entityType === "team") {
|
||||
// Team-specific logic
|
||||
} else if (entityType === "user") {
|
||||
// User-specific logic
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (Factory pattern with specialized services):**
|
||||
|
||||
```typescript
|
||||
// Factory makes the decision at entry point
|
||||
class BillingServiceFactory {
|
||||
static async createService(entityId: number): Promise<BillingService> {
|
||||
const entity = await determineEntityType(entityId);
|
||||
|
||||
switch (entity.type) {
|
||||
case "organization":
|
||||
return new OrganizationBillingService(entity);
|
||||
case "team":
|
||||
return new TeamBillingService(entity);
|
||||
default:
|
||||
return new UserBillingService(entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Each service handles ONLY its specific logic - no conditionals
|
||||
class OrganizationBillingService extends BillingService {
|
||||
async processPayment() {
|
||||
// Only organization logic here - clean and focused
|
||||
}
|
||||
}
|
||||
|
||||
class TeamBillingService extends BillingService {
|
||||
async processPayment() {
|
||||
// Only team logic here - clean and focused
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- Services stay focused with one responsibility
|
||||
- Changes are isolated to specific service implementations
|
||||
- Testing is straightforward - test each service independently
|
||||
- New requirements don't pollute existing code
|
||||
|
||||
**Guidelines:**
|
||||
- Push conditionals up to controllers, factories, or routing logic
|
||||
- Keep services pure and focused on a single responsibility
|
||||
- Prefer polymorphism over conditionals
|
||||
- Watch for if statement accumulation during code review
|
||||
|
||||
Reference: [Cal.diy Engineering Blog](https://cal.com/blog/engineering-in-2026-and-beyond)
|
||||
@@ -0,0 +1,240 @@
|
||||
---
|
||||
title: Trigger.dev Task Implementation
|
||||
impact: HIGH
|
||||
impactDescription: Consistent Trigger.dev patterns ensure reliable async task execution across web and API v2
|
||||
tags: trigger.dev, async, tasker, concurrency, cron, scheduled-tasks
|
||||
---
|
||||
|
||||
# Trigger.dev Task Implementation
|
||||
|
||||
Trigger.dev is the async task runner used in both `apps/web` and `apps/api/v2`. Tasks can be disabled via the `ENABLE_ASYNC_TASKER` env var (set to `"false"` to fall back to synchronous execution).
|
||||
|
||||
## Tasker Architecture
|
||||
|
||||
Every Trigger.dev feature follows the Tasker pattern with these layers:
|
||||
|
||||
```
|
||||
packages/features/<domain>/lib/tasker/
|
||||
types.ts # ITasker interface + payload types
|
||||
<Domain>Tasker.ts # Tasker subclass (dispatches async or sync)
|
||||
<Domain>TriggerTasker.ts # Async implementation (calls .trigger())
|
||||
<Domain>SyncTasker.ts # Sync fallback (executes inline)
|
||||
<Domain>TaskService.ts # Business logic consumed by both
|
||||
trigger/
|
||||
config.ts # Queue + retry + machine config
|
||||
schema.ts # Zod payload schema
|
||||
<task-name>.ts # schemaTask definition
|
||||
```
|
||||
|
||||
Reference implementation: `packages/features/calendars/lib/tasker/` — see `CalendarsTasker.ts` for the Tasker subclass pattern and `trigger/config.ts` for queue/retry/machine configuration
|
||||
|
||||
## Creating a New Task
|
||||
|
||||
### 1. Define the Zod schema for payload validation
|
||||
|
||||
Always use `schemaTask` with a Zod schema for payload validation:
|
||||
|
||||
```typescript
|
||||
// trigger/schema.ts
|
||||
import { z } from "zod";
|
||||
|
||||
export const myTaskSchema = z.object({
|
||||
userId: z.number(),
|
||||
});
|
||||
```
|
||||
|
||||
### 2. Configure queue, retry, and machine
|
||||
|
||||
See `packages/features/calendars/lib/tasker/trigger/config.ts` for a real example.
|
||||
|
||||
```typescript
|
||||
// trigger/config.ts
|
||||
import { type schemaTask, queue } from "@trigger.dev/sdk";
|
||||
|
||||
type MyTask = Pick<Parameters<typeof schemaTask>[0], "machine" | "retry" | "queue">;
|
||||
|
||||
export const myQueue = queue({
|
||||
name: "my-domain",
|
||||
concurrencyLimit: 10,
|
||||
});
|
||||
|
||||
export const myTaskConfig: MyTask = {
|
||||
machine: "small-2x",
|
||||
queue: myQueue,
|
||||
retry: {
|
||||
maxAttempts: 3,
|
||||
factor: 2,
|
||||
minTimeoutInMs: 60000,
|
||||
maxTimeoutInMs: 300000,
|
||||
randomize: true,
|
||||
outOfMemory: {
|
||||
machine: "medium-1x",
|
||||
},
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
### 3. Define the task using schemaTask
|
||||
|
||||
```typescript
|
||||
// trigger/my-task.ts
|
||||
import { schemaTask, type TaskWithSchema } from "@trigger.dev/sdk";
|
||||
import type { z } from "zod";
|
||||
|
||||
import { myTaskConfig } from "./config";
|
||||
import { myTaskSchema } from "./schema";
|
||||
|
||||
export const MY_TASK_JOB_ID = "domain.my-task";
|
||||
|
||||
export const myTask: TaskWithSchema<typeof MY_TASK_JOB_ID, typeof myTaskSchema> =
|
||||
schemaTask({
|
||||
id: MY_TASK_JOB_ID,
|
||||
...myTaskConfig,
|
||||
schema: myTaskSchema,
|
||||
run: async (payload: z.infer<typeof myTaskSchema>) => {
|
||||
const { getMyService } = await import("@calcom/features/<domain>/di/<container>");
|
||||
const service = getMyService();
|
||||
await service.execute(payload);
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### 4. For scheduled (cron) tasks, use `schedules.task`
|
||||
|
||||
```typescript
|
||||
import { schedules } from "@trigger.dev/sdk";
|
||||
|
||||
export const myScheduledTask = schedules.task({
|
||||
id: "domain.my-scheduled-task",
|
||||
...myTaskConfig,
|
||||
cron: {
|
||||
pattern: "0 0 1 * *",
|
||||
timezone: "UTC",
|
||||
},
|
||||
run: async (payload) => {
|
||||
// task logic
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Reference: [Trigger.dev Scheduled Tasks](https://trigger.dev/docs/tasks/scheduled)
|
||||
|
||||
## Concurrency Configuration
|
||||
|
||||
Concurrency limits control how many task runs execute in parallel within a queue. Getting this right is critical for production stability.
|
||||
|
||||
**How to estimate concurrency:**
|
||||
1. Analyze production logs for the number of requests per minute that will trigger the task (from both `apps/web` and `apps/api/v2`)
|
||||
2. Measure the average execution time of the task
|
||||
3. Factor in burst patterns (e.g., peak booking hours)
|
||||
|
||||
**Guidelines:**
|
||||
- Time-sensitive tasks (emails, webhooks) need higher concurrency
|
||||
- Background jobs that are not time-sensitive can use lower concurrency
|
||||
- Start conservative and increase based on production metrics
|
||||
|
||||
See `packages/features/calendars/lib/tasker/trigger/config.ts` (`concurrencyLimit: 10`) and `packages/features/webhooks/lib/tasker/trigger/config.ts` (`concurrencyLimit: 20`) for real examples of how concurrency is tuned per domain.
|
||||
|
||||
Reference: [Trigger.dev Concurrency & Queues](https://trigger.dev/docs/queue-concurrency)
|
||||
|
||||
## Local Development
|
||||
|
||||
1. Set env vars:
|
||||
```
|
||||
TRIGGER_SECRET_KEY=<your-trigger-secret>
|
||||
TRIGGER_API_URL=https://api.trigger.dev
|
||||
ENABLE_ASYNC_TASKER="true"
|
||||
```
|
||||
|
||||
2. Run the Trigger.dev CLI from the features package:
|
||||
```bash
|
||||
cd packages/features && npx trigger.dev@latest dev --analyze
|
||||
```
|
||||
|
||||
3. Keep the CLI running while developing. Tasks appear in the Trigger.dev dashboard under **Tasks**.
|
||||
|
||||
## Before Merging
|
||||
|
||||
1. **Deploy to staging**: `cd packages/features && yarn deploy:trigger:staging`
|
||||
2. **Test on Trigger.dev dashboard**: Switch to staging environment and use the **Test** tab
|
||||
3. **Right-size machines**: Start with the smallest machine; increase only if you see `OutOfMemory` errors
|
||||
4. **Set retry with OOM handling**: Always include `outOfMemory` in retry config with a larger machine than the default
|
||||
5. **Set concurrency**: Analyze production request volume and task completion time to determine the appropriate `concurrencyLimit`
|
||||
6. **Cherry-pick caveat**: Cherry-picking does not redeploy Trigger.dev tasks. Only the `draft-release` CI action does. If cherry-picking a change that impacts Trigger.dev tasks, manually promote the new version in the Trigger.dev deployment dashboard after the fix is deployed
|
||||
|
||||
## Common Mistakes
|
||||
|
||||
**Using `task` instead of `schemaTask`:**
|
||||
|
||||
```typescript
|
||||
// Bad - no payload validation
|
||||
import { task } from "@trigger.dev/sdk";
|
||||
export const myTask = task({
|
||||
id: "my-task",
|
||||
run: async (payload: any) => { ... },
|
||||
});
|
||||
|
||||
// Good - validated payload with Zod
|
||||
import { schemaTask } from "@trigger.dev/sdk";
|
||||
export const myTask = schemaTask({
|
||||
id: "my-task",
|
||||
schema: myTaskSchema,
|
||||
run: async (payload) => { ... },
|
||||
});
|
||||
```
|
||||
|
||||
**Missing retry or queue config:**
|
||||
|
||||
```typescript
|
||||
// Bad - no retry or queue, will use defaults
|
||||
export const myTask = schemaTask({
|
||||
id: "my-task",
|
||||
schema: myTaskSchema,
|
||||
run: async (payload) => { ... },
|
||||
});
|
||||
|
||||
// Good - explicit config from shared config file
|
||||
import { myTaskConfig } from "./config";
|
||||
|
||||
export const myTask = schemaTask({
|
||||
id: "my-task",
|
||||
...myTaskConfig,
|
||||
schema: myTaskSchema,
|
||||
run: async (payload) => { ... },
|
||||
});
|
||||
```
|
||||
|
||||
**Importing eagerly inside task files instead of using dynamic imports:**
|
||||
|
||||
```typescript
|
||||
// Bad - eager import of heavy modules at file scope
|
||||
import { MyService } from "@calcom/features/domain/service/MyService";
|
||||
|
||||
export const myTask = schemaTask({
|
||||
id: "my-task",
|
||||
schema: myTaskSchema,
|
||||
run: async (payload) => {
|
||||
const service = new MyService();
|
||||
await service.execute(payload);
|
||||
},
|
||||
});
|
||||
|
||||
// Good - dynamic import inside run function
|
||||
export const myTask = schemaTask({
|
||||
id: "my-task",
|
||||
schema: myTaskSchema,
|
||||
run: async (payload) => {
|
||||
const { getMyService } = await import("@calcom/features/domain/di/container");
|
||||
const service = getMyService();
|
||||
await service.execute(payload);
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## Key References
|
||||
|
||||
- Trigger.dev docs: [Concurrency & Queues](https://trigger.dev/docs/queue-concurrency), [Scheduled Tasks](https://trigger.dev/docs/tasks/scheduled), [schemaTask](https://trigger.dev/docs/tasks/schemaTask)
|
||||
- Example taskers in the codebase:
|
||||
- `packages/features/calendars/lib/tasker/` (standard pattern)
|
||||
- `packages/features/webhooks/lib/tasker/` (webhook delivery)
|
||||
- `packages/features/ee/billing/service/proration/tasker/` (scheduled cron task)
|
||||
@@ -0,0 +1,54 @@
|
||||
---
|
||||
title: Avoid O(n²) Algorithms - Design for Enterprise Scale
|
||||
impact: CRITICAL
|
||||
impactDescription: Prevents performance collapse at scale
|
||||
tags: performance, algorithms, complexity, scale
|
||||
---
|
||||
|
||||
## Avoid O(n²) Algorithms - Design for Enterprise Scale
|
||||
|
||||
**Impact: CRITICAL**
|
||||
|
||||
We build for large organizations and teams. What works fine with 10 users or 50 records can collapse under the weight of enterprise scale. Performance is not something we optimize later. It's something we build correctly from the start.
|
||||
|
||||
When building features, always ask: "How does this behave with 1,000 users? 10,000 records? 100,000 operations?"
|
||||
|
||||
**Common O(n²) patterns to avoid:**
|
||||
- Nested array iterations (`.map` inside `.map`, `.forEach` inside `.forEach`)
|
||||
- Array methods like `.some`, `.find`, or `.filter` inside loops or callbacks
|
||||
- Checking every item against every other item without optimization
|
||||
- Chained filters or nested mapping over large lists
|
||||
|
||||
**Incorrect (O(n²) - exponential slowdown):**
|
||||
|
||||
```typescript
|
||||
// Bad: O(n²) - checks every slot against every busy time
|
||||
const available = availableSlots.filter(slot => {
|
||||
return !busyTimes.some(busy => checkOverlap(slot, busy));
|
||||
});
|
||||
|
||||
// For 100 slots and 50 busy periods: 5,000 checks
|
||||
// For 500 slots and 200 busy periods: 100,000 checks (20x increase!)
|
||||
```
|
||||
|
||||
**Correct (O(n log n) - scales gracefully):**
|
||||
|
||||
```typescript
|
||||
// Good: O(n log n) - sort once, break early
|
||||
const sortedBusy = [...busyTimes].sort((a, b) => a.start - b.start);
|
||||
|
||||
const available = availableSlots.filter(slot => {
|
||||
// Binary search or early exit
|
||||
const index = binarySearch(sortedBusy, slot.start);
|
||||
return !hasOverlapAt(sortedBusy, index, slot);
|
||||
});
|
||||
```
|
||||
|
||||
**Better data structures and algorithms:**
|
||||
- **Sorting + early exit**: Sort data once, break out of loops when remaining items won't match
|
||||
- **Binary search**: Use for lookups in sorted arrays instead of linear scans
|
||||
- **Two-pointer techniques**: For merging or intersecting sorted sequences
|
||||
- **Hash maps/sets**: Use for O(1) lookups instead of `.find` or `.includes` on arrays
|
||||
- **Interval trees**: For scheduling, availability, and range queries
|
||||
|
||||
Reference: [Cal.diy Engineering Blog](https://cal.com/blog/engineering-in-2026-and-beyond)
|
||||
@@ -0,0 +1,53 @@
|
||||
---
|
||||
title: Day.js Performance Guidelines
|
||||
impact: HIGH
|
||||
impactDescription: Significant performance improvement in date-heavy operations
|
||||
tags: performance, dates, dayjs
|
||||
---
|
||||
|
||||
## Day.js Performance Guidelines
|
||||
|
||||
**Impact: HIGH (Significant performance improvement in date-heavy operations)**
|
||||
|
||||
Day.js with the `@calcom/dayjs` wrapper is heavy because it pre-loads all plugins including locale handling. Use alternatives when strict timezone awareness isn't required.
|
||||
|
||||
**Incorrect (using Day.js unnecessarily):**
|
||||
|
||||
```typescript
|
||||
// Slow in performance-critical code (loops)
|
||||
dates.map((date) => dayjs(date).add(1, "day").format());
|
||||
|
||||
// Using Dayjs for simple date operations
|
||||
const startOfMonth = dayjs().startOf("month");
|
||||
```
|
||||
|
||||
**Correct (using performant alternatives):**
|
||||
|
||||
```typescript
|
||||
// Use .utc() for better performance when timezone doesn't matter
|
||||
dates.map((date) => dayjs.utc(date).add(1, "day").format());
|
||||
|
||||
// Use native Date when possible
|
||||
dates.map((date) => new Date(date.valueOf() + 24 * 60 * 60 * 1000));
|
||||
|
||||
// Use date-fns for simple operations
|
||||
import { startOfMonth, endOfDay } from "date-fns";
|
||||
const monthStart = startOfMonth(dateObj);
|
||||
const dayEnd = endOfDay(dateObj);
|
||||
|
||||
// For browser locale, use Intl with i18n
|
||||
const { i18n: { language } } = useLocale();
|
||||
new Intl.DateTimeFormat(language).format(date);
|
||||
```
|
||||
|
||||
**When to use Day.js:**
|
||||
- When you need strict timezone awareness (e.g., in the Booker)
|
||||
- When working with complex timezone conversions
|
||||
- When the performance impact is negligible (non-loop operations)
|
||||
|
||||
**When to avoid Day.js:**
|
||||
- Simple date arithmetic
|
||||
- Date formatting without timezone concerns
|
||||
- Performance-critical loops over dates
|
||||
|
||||
Reference: [Cal.diy Engineering Standards](https://cal.com/blog/engineering-in-2026-and-beyond)
|
||||
@@ -0,0 +1,58 @@
|
||||
---
|
||||
title: Handle NP-Hard Scheduling Problems Carefully
|
||||
impact: HIGH
|
||||
impactDescription: Prevents exponential blowup in scheduling operations
|
||||
tags: performance, scheduling, algorithms, np-hard
|
||||
---
|
||||
|
||||
## Handle NP-Hard Scheduling Problems Carefully
|
||||
|
||||
**Impact: HIGH**
|
||||
|
||||
Scheduling problems are fundamentally NP-hard. This means that as the number of constraints, participants, or time slots grows, the computational complexity can explode exponentially. Most optimal scheduling algorithms have worst-case exponential time complexity, making algorithm choice absolutely critical.
|
||||
|
||||
**Real-world implications:**
|
||||
- Finding the optimal meeting time for 10 people across 3 time zones with individual availability constraints is computationally expensive
|
||||
- Adding conflict detection, buffers, and other options amplifies the problem
|
||||
- Poor algorithm choices that work fine for small teams become completely unusable for large organizations
|
||||
- What takes milliseconds for 5 users might take many seconds for organizations
|
||||
|
||||
**Strategies for managing NP-hard complexity:**
|
||||
|
||||
```typescript
|
||||
// Use approximation algorithms
|
||||
async function findMeetingTime(participants: User[], duration: number) {
|
||||
// Find "good enough" solution quickly rather than perfect solution slowly
|
||||
const approximateSlots = await findApproximateAvailability(participants, {
|
||||
maxIterations: 1000,
|
||||
timeout: 500, // ms
|
||||
});
|
||||
|
||||
return approximateSlots[0]; // Return first good-enough option
|
||||
}
|
||||
|
||||
// Implement aggressive caching
|
||||
const cachedAvailability = new LRUCache<string, Availability>({
|
||||
max: 10000,
|
||||
ttl: 1000 * 60 * 5, // 5 minutes
|
||||
});
|
||||
|
||||
// Pre-compute common scenarios during off-peak hours
|
||||
async function precomputeTeamAvailability(teamId: number) {
|
||||
// Run during low-traffic periods
|
||||
const team = await teamRepository.findById(teamId);
|
||||
const availability = await computeTeamAvailability(team);
|
||||
await cache.set(`team:${teamId}:availability`, availability);
|
||||
}
|
||||
```
|
||||
|
||||
**Key strategies:**
|
||||
- Use approximation algorithms that find "good enough" solutions quickly
|
||||
- Implement aggressive caching of computed schedules and availability
|
||||
- Pre-compute common scenarios during off-peak hours
|
||||
- Break large scheduling problems into smaller, more manageable chunks
|
||||
- Set reasonable timeout limits and fallback to simpler algorithms when needed
|
||||
|
||||
This is why performance isn't just a nice-to-have in scheduling software. It's the foundation that determines whether your system can scale to enterprise needs.
|
||||
|
||||
Reference: [Cal.diy Engineering Blog](https://cal.com/blog/engineering-in-2026-and-beyond)
|
||||
@@ -0,0 +1,35 @@
|
||||
---
|
||||
title: Avoid Barrel Imports
|
||||
impact: MEDIUM
|
||||
impactDescription: Improves tree-shaking and reduces bundle size
|
||||
tags: imports, performance, bundling
|
||||
---
|
||||
|
||||
## Avoid Barrel Imports
|
||||
|
||||
**Impact: MEDIUM (Improves tree-shaking and reduces bundle size)**
|
||||
|
||||
Barrel files (index.ts that re-export from multiple modules) can hurt tree-shaking and increase bundle sizes. Import directly from source files instead.
|
||||
|
||||
**Incorrect (importing from barrel files):**
|
||||
|
||||
```typescript
|
||||
// Importing from index.ts barrel files
|
||||
import { BookingService, UserService } from "./services";
|
||||
|
||||
// Importing from @calcom/ui barrel
|
||||
import { Button } from "@calcom/ui";
|
||||
```
|
||||
|
||||
**Correct (importing directly from source):**
|
||||
|
||||
```typescript
|
||||
// Import directly from source files
|
||||
import { BookingService } from "./services/BookingService";
|
||||
import { UserService } from "./services/UserService";
|
||||
|
||||
// Import directly from component path
|
||||
import { Button } from "@calcom/ui/components/button";
|
||||
```
|
||||
|
||||
Reference: [Cal.diy Engineering Standards](https://cal.com/blog/engineering-in-2026-and-beyond)
|
||||
@@ -0,0 +1,51 @@
|
||||
---
|
||||
title: Code Comment Guidelines
|
||||
impact: MEDIUM
|
||||
impactDescription: Excessive comments add noise; missing comments hurt maintainability
|
||||
tags: comments, documentation, readability
|
||||
---
|
||||
|
||||
# Code Comment Guidelines
|
||||
|
||||
## General Principle
|
||||
|
||||
Keep comments limited and avoid obvious ones. Comments should explain "why" not "what" - the code itself should be clear enough to explain what it does.
|
||||
|
||||
## When to Comment
|
||||
|
||||
- Business decisions or domain logic that isn't obvious from the code
|
||||
- Workarounds or hacks with explanation of why they're needed
|
||||
- Non-obvious performance optimizations
|
||||
- Important security considerations
|
||||
- Troubleshooting context (e.g., why a particular approach was chosen after hitting issues)
|
||||
|
||||
If none of these apply, skip the comment entirely. The function name, parameters, and return type should speak for themselves.
|
||||
|
||||
## When NOT to Comment
|
||||
|
||||
```typescript
|
||||
// ❌ Bad - Obvious comment
|
||||
// Get the user
|
||||
const user = await getUser(userId);
|
||||
|
||||
// ❌ Bad - Restating the code
|
||||
// Loop through bookings
|
||||
for (const booking of bookings) {
|
||||
// Process booking
|
||||
processBooking(booking);
|
||||
}
|
||||
```
|
||||
|
||||
## Good Examples
|
||||
|
||||
```typescript
|
||||
// ✅ Good - Explains why, not what
|
||||
// We need to fetch availability before slots because the timezone
|
||||
// conversion depends on the user's configured availability rules
|
||||
const availability = await getAvailability(userId);
|
||||
const slots = convertToSlots(availability, timezone);
|
||||
|
||||
// ✅ Good - Documents a non-obvious constraint
|
||||
// Google Calendar API has a 2500 event limit per sync request
|
||||
const BATCH_SIZE = 2500;
|
||||
```
|
||||
@@ -0,0 +1,36 @@
|
||||
---
|
||||
title: Code Review Focus
|
||||
impact: MEDIUM
|
||||
impactDescription: Focused reviews are more useful than scattered feedback
|
||||
tags: code-review, workflow
|
||||
---
|
||||
|
||||
# Code Review Focus
|
||||
|
||||
## When Asked to Review a PR
|
||||
|
||||
Focus on providing a clear summary of what the PR is doing and its core functionality.
|
||||
|
||||
**Avoid getting sidetracked by:**
|
||||
- CI failures
|
||||
- Testing issues
|
||||
- Technical implementation details (unless specifically requested)
|
||||
|
||||
## Good Review Structure
|
||||
|
||||
1. **Summary**: What does this PR do?
|
||||
2. **Core changes**: What are the main code changes?
|
||||
3. **Impact**: What parts of the system does this affect?
|
||||
|
||||
## What to Look For
|
||||
|
||||
- Does the code do what it claims to do?
|
||||
- Are there any obvious bugs or edge cases?
|
||||
- Does it follow Cal.diy coding standards?
|
||||
- Is the change appropriately scoped?
|
||||
|
||||
## What to Skip (Unless Asked)
|
||||
|
||||
- Nitpicks about style (Biome handles this)
|
||||
- Suggestions for refactoring unrelated code
|
||||
- Deep dives into implementation details
|
||||
@@ -0,0 +1,52 @@
|
||||
---
|
||||
title: Error Handling Patterns
|
||||
impact: HIGH
|
||||
impactDescription: Proper error handling ensures debuggable and secure code
|
||||
tags: errors, trpc, services, repositories
|
||||
---
|
||||
|
||||
# Error Handling Patterns
|
||||
|
||||
## Descriptive Errors
|
||||
|
||||
```typescript
|
||||
// ✅ Good - Descriptive error with context
|
||||
throw new Error(`Unable to create booking: User ${userId} has no available time slots for ${date}`);
|
||||
|
||||
// ❌ Bad - Generic error
|
||||
throw new Error("Booking failed");
|
||||
```
|
||||
|
||||
## ErrorWithCode vs TRPCError
|
||||
|
||||
Use `ErrorWithCode` for files that are not directly coupled to tRPC. The tRPC package has a middleware called `errorConversionMiddleware` that automatically converts `ErrorWithCode` instances into `TRPCError` instances.
|
||||
|
||||
### In Non-tRPC Files (services, repositories, utilities)
|
||||
|
||||
```typescript
|
||||
import { ErrorCode } from "@calcom/lib/errorCodes";
|
||||
import { ErrorWithCode } from "@calcom/lib/errors";
|
||||
|
||||
// Option 1: Using constructor with ErrorCode enum
|
||||
throw new ErrorWithCode(ErrorCode.BookingNotFound, "Booking not found");
|
||||
|
||||
// Option 2: Using the Factory pattern for common HTTP errors
|
||||
throw ErrorWithCode.Factory.Forbidden("You don't have permission to view this");
|
||||
throw ErrorWithCode.Factory.NotFound("Resource not found");
|
||||
throw ErrorWithCode.Factory.BadRequest("Invalid input");
|
||||
```
|
||||
|
||||
### In tRPC Routers Only
|
||||
|
||||
```typescript
|
||||
import { TRPCError } from "@trpc/server";
|
||||
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Invalid booking time slot",
|
||||
});
|
||||
```
|
||||
|
||||
## packages/features Import Restrictions
|
||||
|
||||
Files in `packages/features/**` should NOT import from `@calcom/trpc`. This keeps the features package decoupled from the tRPC layer, making the code more reusable and testable. Use `ErrorWithCode` for error handling in these files.
|
||||
@@ -0,0 +1,49 @@
|
||||
---
|
||||
title: Import and Export Patterns
|
||||
impact: MEDIUM
|
||||
impactDescription: Incorrect imports cause build failures and bundle bloat
|
||||
tags: imports, exports, modules, app-store
|
||||
---
|
||||
|
||||
# Import and Export Patterns
|
||||
|
||||
## Named vs Default Exports
|
||||
|
||||
When working with imports in the Cal.diy codebase, particularly in app-store integrations, pay attention to whether modules use named exports or default exports.
|
||||
|
||||
Many services like VideoApiAdapter, CalendarService, and PaymentService are exported as named exports, but the actual export name may differ from the generic service type.
|
||||
|
||||
```typescript
|
||||
// ✅ Good - Verify actual export name and use named import
|
||||
import { AppleCalendarService } from "./applecalendar/lib/CalendarService";
|
||||
|
||||
// With renaming if needed
|
||||
import { AppleCalendarService as ApplecalendarCalendarService } from "./applecalendar/lib/CalendarService";
|
||||
|
||||
// ❌ Bad - Assuming default export without checking
|
||||
import CalendarService from "./applecalendar/lib/CalendarService";
|
||||
```
|
||||
|
||||
## Generated Files
|
||||
|
||||
When fixing imports in Cal.diy's generated files (like `packages/app-store/apps.browser-*.generated.tsx`), always check the actual exports in the source files first.
|
||||
|
||||
For EventTypeAppCardInterface components, they likely use named exports rather than default exports, requiring:
|
||||
|
||||
```typescript
|
||||
import * as ComponentName from "./path";
|
||||
// instead of
|
||||
import ComponentName from "./path";
|
||||
```
|
||||
|
||||
## Factory Function Naming
|
||||
|
||||
When creating factory functions that replace class exports, use the naming convention `Build[ServiceName]` instead of just `[ServiceName]`:
|
||||
|
||||
```typescript
|
||||
// ✅ Good - Clear factory function naming
|
||||
export function BuildPaymentService() { ... }
|
||||
|
||||
// ❌ Bad - Confusing with class export
|
||||
export function PaymentService() { ... }
|
||||
```
|
||||
@@ -0,0 +1,40 @@
|
||||
---
|
||||
title: Minimize Follow-up PRs for Small Refactors
|
||||
impact: HIGH
|
||||
impactDescription: Prevents technical debt accumulation
|
||||
tags: quality, pr, refactoring, technical-debt
|
||||
---
|
||||
|
||||
## Minimize Follow-up PRs for Small Refactors
|
||||
|
||||
**Impact: HIGH**
|
||||
|
||||
Follow-up PRs for minor improvements rarely materialize. Instead, they accumulate as technical debt that burdens us months or years later. If a small refactor can be done now, do it now.
|
||||
|
||||
**Incorrect:**
|
||||
|
||||
```typescript
|
||||
// PR comment: "I'll fix this naming in a follow-up PR"
|
||||
const d = getUserData(); // Bad variable name
|
||||
const r = processData(d); // Another bad name
|
||||
|
||||
// The follow-up PR never happens, and now we have unclear code
|
||||
```
|
||||
|
||||
**Correct:**
|
||||
|
||||
```typescript
|
||||
// Fix it in the same PR
|
||||
const userData = getUserData();
|
||||
const processedResult = processData(userData);
|
||||
```
|
||||
|
||||
**When follow-ups are acceptable:**
|
||||
- Substantial changes that genuinely warrant separate PRs
|
||||
- Exceptional, urgent cases where the current PR must ship immediately
|
||||
- Changes that require additional review from different stakeholders
|
||||
|
||||
**The principle:**
|
||||
The "slowness" of doing things right is an investment, not a cost. Code that's architected correctly from the start doesn't need massive refactors later.
|
||||
|
||||
Reference: [Cal.diy Engineering Blog](https://cal.com/blog/engineering-in-2026-and-beyond)
|
||||
@@ -0,0 +1,36 @@
|
||||
---
|
||||
title: PR Creation Best Practices
|
||||
impact: HIGH
|
||||
impactDescription: PRs that don't follow guidelines slow down review cycles
|
||||
tags: pull-request, code-review, workflow
|
||||
---
|
||||
|
||||
# PR Creation Best Practices
|
||||
|
||||
## Draft Mode
|
||||
|
||||
Create pull requests in draft mode by default, so that a human reviewer can mark it as ready for review only when it is.
|
||||
|
||||
## PR Title
|
||||
|
||||
- Use conventional commits: `feat:`, `fix:`, `refactor:`
|
||||
- Be specific: `fix: handle timezone edge case in booking creation`
|
||||
- Not generic: `fix: booking bug`
|
||||
|
||||
## Size Limits
|
||||
|
||||
- **Large PRs** (>500 lines or >10 files) are not recommended
|
||||
- Split large changes by layer (database, backend, frontend)
|
||||
- Split by feature component (API, UI, integration)
|
||||
|
||||
## PR Requirements
|
||||
|
||||
- PR title must follow Conventional Commits specification
|
||||
- For most PRs, you only need to run linting and type checking
|
||||
- E2E tests will only run if PR has "ready-for-e2e" label
|
||||
|
||||
## Before Pushing
|
||||
|
||||
1. Run `yarn type-check:ci --force` to check types
|
||||
2. Run `yarn biome check --write .` to lint and format
|
||||
3. Run relevant tests locally
|
||||
@@ -0,0 +1,56 @@
|
||||
---
|
||||
title: Code Review Checklist
|
||||
impact: HIGH
|
||||
impactDescription: Ensures consistent code quality across all reviews
|
||||
tags: quality, code-review, checklist
|
||||
---
|
||||
|
||||
## Code Review Checklist
|
||||
|
||||
**Impact: HIGH**
|
||||
|
||||
A comprehensive checklist for code reviewers to ensure consistent quality standards.
|
||||
|
||||
### File Naming Conventions
|
||||
|
||||
- **Repository Files**: Must include `Repository` suffix, prefix with technology if applicable (e.g., `PrismaAppRepository.ts`), use PascalCase matching exported class
|
||||
- **Service Files**: Must include `Service` suffix, use PascalCase matching exported class, avoid generic names (e.g., `MembershipService.ts`)
|
||||
- **New files**: Avoid dot-suffixes like `.service.ts` or `.repository.ts`; reserve `.test.ts`, `.spec.ts`, `.types.ts` for their specific purposes
|
||||
|
||||
### Code Quality
|
||||
|
||||
- Prefer early returns. It is recommended to throw/return early so we can ensure null-checks and prevent further nesting.
|
||||
- Prefer Composition over Prop Drilling. Instead of relying on prop drilling, let's try to take advantage of react children feature.
|
||||
- For Prisma queries:
|
||||
- Only select data you need
|
||||
1) Increased performance overhead: When you select all columns from a table, Prisma retrieves all the data associated with each row, which can result in a significant performance impact. This includes reading unnecessary columns and transferring a large volume of data between your application and the database.
|
||||
2) Unnecessary data exposure: Selecting all columns exposes sensitive data that might not be required by your application. This can pose security risks, especially if the table contains sensitive user information or proprietary data. (In our case app credentials and booking information)
|
||||
- TLDR: Never use Includes - use select
|
||||
- Select selects only the fields you specify explicitly
|
||||
- Include selects the relationship AND all the fields of the table the relationship belongs to
|
||||
- Make sure the credential.key field is never returned back from tRPC endpoints or APIs
|
||||
- Always use t() for text localization in frontend code; direct text embedding should trigger a warning.
|
||||
- Check if there's any O(n^2) logic in backend code; we should aim for O(n log n) or O(n) ideally.
|
||||
- Check if there are circular references introduced. We should never allow these.
|
||||
- Flag excessive Day.js use in performance-critical code. Functions like `.add`, `.diff`, `.isBefore`, and `.isAfter` are slow, especially in timezone mode. Prefer `.utc()` for better performance. Where possible, replace with native Date and direct `.valueOf()` comparisons for faster execution. Recommend using native methods or Day.js `.utc()` consistently in hot paths like loops.
|
||||
|
||||
### API Documentation
|
||||
|
||||
When docs changes are made in /docs/api-reference/v2/openapi.json, ensure the following:
|
||||
- "summary" fields are short and concise
|
||||
- "summary" fields do not end with a period
|
||||
- "summary" fields are written in proper American English
|
||||
|
||||
### API Changes
|
||||
|
||||
When changes to API v2 or v1 are made, ensure there are no breaking changes on existing endpoints. Instead, there needs to be a newly versioned endpoint with the updated functionality but the old one must remain functional.
|
||||
|
||||
### PR Size
|
||||
|
||||
For large pull requests (>500 lines changed or >10 files touched), advise splitting into smaller, focused PRs:
|
||||
- Split by feature boundaries: separate different features or user stories
|
||||
- Split by layer/component: frontend changes, backend changes, database migrations, and tests in separate PRs
|
||||
- Split by dependency chain: create PRs that can be merged sequentially
|
||||
- Split by file/module: group related file changes together
|
||||
- Suggested pattern: Database migrations → Backend logic → Frontend components → Integration tests
|
||||
- Benefits: easier review, faster feedback, reduced conflicts, easier rollback, better history
|
||||
@@ -0,0 +1,43 @@
|
||||
---
|
||||
title: Prioritize Clarity Over Cleverness
|
||||
impact: HIGH
|
||||
impactDescription: Reduces cognitive load and improves maintainability
|
||||
tags: quality, simplicity, readability
|
||||
---
|
||||
|
||||
## Prioritize Clarity Over Cleverness
|
||||
|
||||
**Impact: HIGH**
|
||||
|
||||
The goal is code that is easy to read and understand quickly, not elegant complexity. Simple systems reduce the cognitive load for every engineer.
|
||||
|
||||
**Questions to ask yourself:**
|
||||
- Am I actually solving the problem at hand?
|
||||
- Am I thinking too much about possible future use cases?
|
||||
- Have I considered at least 1 other alternative for solving this? How does it compare?
|
||||
|
||||
**Incorrect (clever but hard to understand):**
|
||||
|
||||
```typescript
|
||||
// Clever one-liner that's hard to parse
|
||||
const result = data.reduce((a, b) => ({...a, [b.id]: (a[b.id] || []).concat(b)}), {});
|
||||
```
|
||||
|
||||
**Correct (clear and readable):**
|
||||
|
||||
```typescript
|
||||
// Clear, step-by-step approach
|
||||
const groupedById: Record<string, Item[]> = {};
|
||||
|
||||
for (const item of data) {
|
||||
if (!groupedById[item.id]) {
|
||||
groupedById[item.id] = [];
|
||||
}
|
||||
groupedById[item.id].push(item);
|
||||
}
|
||||
```
|
||||
|
||||
**Important note:**
|
||||
Simple doesn't mean lacking in features. Just because our goal is to create simple systems, this doesn't mean they should feel anemic and lacking obvious functionality.
|
||||
|
||||
Reference: [Cal.diy Engineering Blog](https://cal.com/blog/engineering-in-2026-and-beyond)
|
||||
@@ -0,0 +1,40 @@
|
||||
---
|
||||
title: Address All Nits Before Merging
|
||||
impact: HIGH
|
||||
impactDescription: Prevents codebase degradation over time
|
||||
tags: quality, code-review, standards
|
||||
---
|
||||
|
||||
## Address All Nits Before Merging
|
||||
|
||||
**Impact: HIGH**
|
||||
|
||||
Don't let PRs through with a lot of nits just to avoid being "the bad person." This is precisely how codebases become sloppy over time. Code review is not about being nice. It's about maintaining the quality standards our infrastructure demands.
|
||||
|
||||
**Incorrect approach:**
|
||||
|
||||
```
|
||||
Reviewer: "This variable name could be clearer, but it's fine I guess"
|
||||
Reviewer: "We usually use early returns here, but this works"
|
||||
Reviewer: "Approved with minor suggestions"
|
||||
// PR merged with multiple small issues
|
||||
```
|
||||
|
||||
**Correct approach:**
|
||||
|
||||
```
|
||||
Reviewer: "Please rename `d` to `userData` for clarity"
|
||||
Reviewer: "Please refactor to use early returns per our standards"
|
||||
Reviewer: "Requesting changes - please address before merging"
|
||||
// PR updated to meet all standards before merge
|
||||
```
|
||||
|
||||
**The principle:**
|
||||
Every nitpick matters. Every pattern violation matters. Address them before merging, not after. We hold each other accountable for quality because cutting corners might feel faster in the moment, but it creates problems that slow everyone down later.
|
||||
|
||||
**Make it normal to challenge poor decisions, respectfully:**
|
||||
- If someone says "let's just hard-code this for now," ask "what would it take to do it the proper way the first time?"
|
||||
- If someone wants to commit untested code, push back
|
||||
- If someone suggests copying and pasting instead of creating a proper abstraction, call it out respectfully
|
||||
|
||||
Reference: [Cal.diy Engineering Blog](https://cal.com/blog/engineering-in-2026-and-beyond)
|
||||
@@ -0,0 +1,50 @@
|
||||
---
|
||||
title: Key File Locations
|
||||
impact: LOW
|
||||
impactDescription: Quick reference for finding important files
|
||||
tags: reference, navigation, file-locations
|
||||
---
|
||||
|
||||
# Key File Locations
|
||||
|
||||
## UI Components
|
||||
|
||||
- Event types page: `apps/web/modules/event-types/views/event-types-listing-view.tsx`
|
||||
- Bookings page: `apps/web/modules/bookings/views/bookings-view.tsx`
|
||||
- Shared UI patterns (tabs, search bars, filter buttons) should maintain consistent alignment across views
|
||||
|
||||
## Database
|
||||
|
||||
- Schema: `packages/prisma/schema.prisma`
|
||||
- Migrations: `packages/prisma/migrations/`
|
||||
|
||||
## API
|
||||
|
||||
- tRPC routers: `packages/trpc/server/routers/`
|
||||
- API v2 controllers: `apps/api/v2/src/modules/*/controllers/*.controller.ts`
|
||||
- OpenAPI spec: `docs/api-reference/v2/openapi.json` (auto-generated, don't edit manually)
|
||||
|
||||
## Features
|
||||
|
||||
- Workflow constants: `packages/features/ee/workflows/lib/constants.ts`
|
||||
- Round-robin/host prioritization: `packages/features/bookings/lib/getLuckyUser.ts`
|
||||
- Calendar cache: `packages/features/calendar-cache-sql`
|
||||
- DataTable guide: `packages/features/data-table/GUIDE.md`
|
||||
|
||||
## Translations
|
||||
|
||||
- English: `packages/i18n/locales/en/common.json`
|
||||
|
||||
## App Store
|
||||
|
||||
- Generated files: `packages/app-store/*.generated.ts`
|
||||
- CLI tool: `packages/app-store-cli/`
|
||||
|
||||
## File Naming Conventions
|
||||
|
||||
- **Repository files**: `PrismaBookingRepository.ts` (PascalCase with Repository suffix)
|
||||
- **Service files**: `MembershipService.ts` (PascalCase with Service suffix)
|
||||
- **Components**: `BookingForm.tsx` (PascalCase)
|
||||
- **Utilities**: `date-utils.ts` (kebab-case)
|
||||
- **Types**: `Booking.types.ts` (PascalCase with .types.ts suffix)
|
||||
- **Tests**: Same as source file + `.test.ts` or `.spec.ts`
|
||||
@@ -0,0 +1,73 @@
|
||||
---
|
||||
title: Local Development Setup
|
||||
impact: LOW
|
||||
impactDescription: Reference guide for local development environment
|
||||
tags: reference, development, setup
|
||||
---
|
||||
|
||||
# Local Development Setup
|
||||
|
||||
## Initial Setup
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
yarn
|
||||
|
||||
# Set up environment
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
|
||||
Generate required secrets:
|
||||
|
||||
```bash
|
||||
# NEXTAUTH_SECRET
|
||||
openssl rand -base64 32
|
||||
|
||||
# CALENDSO_ENCRYPTION_KEY (must be 32 characters for AES256)
|
||||
openssl rand -base64 24
|
||||
```
|
||||
|
||||
Configure in `.env`:
|
||||
- `DATABASE_URL` - PostgreSQL connection string
|
||||
- `DATABASE_DIRECT_URL` - Same as DATABASE_URL
|
||||
|
||||
## Database Setup
|
||||
|
||||
```bash
|
||||
# Development
|
||||
yarn workspace @calcom/prisma db-migrate
|
||||
|
||||
# Production
|
||||
yarn workspace @calcom/prisma db-deploy
|
||||
```
|
||||
|
||||
## Test Users
|
||||
|
||||
When setting up local development database, it creates test users. The passwords are the same as the username:
|
||||
- `free:free`
|
||||
- `pro:pro`
|
||||
|
||||
## Logging
|
||||
|
||||
Control logging verbosity by setting `NEXT_PUBLIC_LOGGER_LEVEL` in .env:
|
||||
- 0: silly
|
||||
- 1: trace
|
||||
- 2: debug
|
||||
- 3: info
|
||||
- 4: warn
|
||||
- 5: error
|
||||
- 6: fatal
|
||||
|
||||
## API v2 Imports
|
||||
|
||||
If you need to import from `@calcom/features` or `@calcom/trpc` into `apps/api/v2`, use the platform-libraries package instead:
|
||||
|
||||
```typescript
|
||||
// ✅ Good
|
||||
import { SomeService } from "@calcom/platform-libraries";
|
||||
|
||||
// ❌ Bad - Will cause module resolution errors
|
||||
import { SomeService } from "@calcom/features/...";
|
||||
```
|
||||
@@ -0,0 +1,67 @@
|
||||
---
|
||||
title: Maintain 80%+ Test Coverage for New Code
|
||||
impact: HIGH
|
||||
impactDescription: Prevents bugs and enables confident refactoring
|
||||
tags: testing, coverage, quality, ci
|
||||
---
|
||||
|
||||
## Maintain 80%+ Test Coverage for New Code
|
||||
|
||||
**Impact: HIGH**
|
||||
|
||||
Every PR must have near-80%+ test coverage for the code it introduces or modifies. This is enforced automatically in our CI pipeline. If you add 50 lines of new code, those 50 lines must be covered by tests. If you modify an existing function, your changes must be tested.
|
||||
|
||||
**Coverage requirements:**
|
||||
- Overall test coverage: 80%+ for new code
|
||||
- Unit test coverage: Near 100%, especially with AI assistance for generating tests
|
||||
- Global coverage tracking as a key metric that improves over time
|
||||
|
||||
**Incorrect (untested code):**
|
||||
|
||||
```typescript
|
||||
// New function with no tests
|
||||
export function calculateAvailability(user: User, date: Date): TimeSlot[] {
|
||||
// Complex logic here...
|
||||
// No corresponding test file
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (comprehensive tests):**
|
||||
|
||||
```typescript
|
||||
// calculateAvailability.ts
|
||||
export function calculateAvailability(user: User, date: Date): TimeSlot[] {
|
||||
// Complex logic here...
|
||||
}
|
||||
|
||||
// calculateAvailability.test.ts
|
||||
describe("calculateAvailability", () => {
|
||||
it("returns empty array for user with no schedule", () => {
|
||||
const user = createMockUser({ schedules: [] });
|
||||
expect(calculateAvailability(user, new Date())).toEqual([]);
|
||||
});
|
||||
|
||||
it("excludes busy times from available slots", () => {
|
||||
const user = createMockUser({
|
||||
schedules: [mockSchedule],
|
||||
busyTimes: [mockBusyTime],
|
||||
});
|
||||
const slots = calculateAvailability(user, new Date());
|
||||
expect(slots).not.toContainEqual(expect.objectContaining({
|
||||
start: mockBusyTime.start,
|
||||
}));
|
||||
});
|
||||
|
||||
it("handles timezone conversions correctly", () => {
|
||||
// Test timezone edge cases
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
**Addressing the "coverage isn't the full story" argument:**
|
||||
Yes, we know coverage doesn't guarantee perfect tests. We know you can write meaningless tests that hit every line but test nothing meaningful. We know coverage is just one metric among many. But it's surely better to shoot for a high percentage than to have no idea where you are at all.
|
||||
|
||||
**Leverage AI for test generation:**
|
||||
AI can quickly and intelligently build comprehensive test suites. Manual testing is more and more a thing of the past.
|
||||
|
||||
Reference: [Cal.diy Engineering Blog](https://cal.com/blog/engineering-in-2026-and-beyond)
|
||||
@@ -0,0 +1,27 @@
|
||||
---
|
||||
title: Incremental Test Fixing
|
||||
impact: MEDIUM
|
||||
impactDescription: Methodical approach prevents getting overwhelmed by test failures
|
||||
tags: testing, debugging, workflow
|
||||
---
|
||||
|
||||
# Incremental Test Fixing
|
||||
|
||||
## One File at a Time
|
||||
|
||||
When fixing failing tests in the Cal.diy repository, take an incremental approach by addressing one file at a time rather than attempting to fix all issues simultaneously.
|
||||
|
||||
This methodical approach makes it easier to identify and resolve specific issues without getting overwhelmed by the complexity of multiple failing tests across different files.
|
||||
|
||||
## Recommended Order
|
||||
|
||||
1. Run `yarn type-check:ci --force` to identify TypeScript type errors
|
||||
2. Run `yarn test` to identify failing unit tests
|
||||
3. Address both type errors and failing tests before considering the task complete
|
||||
4. Type errors often need to be fixed first as they may be causing the test failures
|
||||
|
||||
## Focus Strategy
|
||||
|
||||
- Focus on getting each file's tests passing completely before moving on to the next file
|
||||
- Fix type errors before test failures - they're often the root cause
|
||||
- Run `yarn prisma generate` if you see missing enum/type errors
|
||||
@@ -0,0 +1,26 @@
|
||||
---
|
||||
title: Mock Implementation Patterns
|
||||
impact: MEDIUM
|
||||
impactDescription: Poor mocks cause flaky tests and false positives
|
||||
tags: testing, mocking, calendar, app-store
|
||||
---
|
||||
|
||||
# Mock Implementation Patterns
|
||||
|
||||
## Calendar Service Mocks
|
||||
|
||||
When mocking calendar services in Cal.diy test files, implement the `Calendar` interface rather than adding individual properties from each specific calendar service type (like `FeishuCalendarService`).
|
||||
|
||||
Since all calendar services implement the `Calendar` interface and are stored in a map, the mock service should also implement this interface to ensure type compatibility.
|
||||
|
||||
## App-Store Integration Mocks
|
||||
|
||||
When mocking app-store resources in Cal.diy tests, prefer implementing simpler mock designs that directly implement the required interfaces rather than trying to match complex deep mock structures created with `mockDeep`.
|
||||
|
||||
This approach is more maintainable and helps resolve type compatibility issues.
|
||||
|
||||
## General Guidance
|
||||
|
||||
- For complex mocks that cause type compatibility issues with deep mocks, consider using simpler fake implementations
|
||||
- When needed, you can modify other mock files to support your implementation
|
||||
- Creative solutions and refactoring to better designs are encouraged when standard mocking causes persistent type errors
|
||||
@@ -0,0 +1,32 @@
|
||||
---
|
||||
title: Playwright E2E Testing
|
||||
impact: HIGH
|
||||
impactDescription: E2E tests catch integration issues before production
|
||||
tags: testing, playwright, e2e
|
||||
---
|
||||
|
||||
# Playwright E2E Testing
|
||||
|
||||
## Running Tests
|
||||
|
||||
Use the command format:
|
||||
|
||||
```bash
|
||||
PLAYWRIGHT_HEADLESS=1 yarn e2e [test-file.e2e.ts]
|
||||
```
|
||||
|
||||
This format includes the proper timezone setting, virtual display server, and uses the repository's e2e runner.
|
||||
|
||||
**Do not use** the standard `yarn playwright test` command.
|
||||
|
||||
## Local Testing First
|
||||
|
||||
Always ensure Playwright tests pass locally before pushing code. The user requires fast local e2e feedback loops instead of relying on CI, which is too slow for development iteration.
|
||||
|
||||
**Never push test code until those tests are passing locally first.**
|
||||
|
||||
## CI Behavior
|
||||
|
||||
- E2E tests will only run if PR has "ready-for-e2e" label
|
||||
- When E2E tests are skipped, the "required" check intentionally fails to prevent merging without E2E
|
||||
- Do not try to fix anything related to skipped E2E tests - this is expected behavior
|
||||
@@ -0,0 +1,37 @@
|
||||
---
|
||||
title: Timezone Handling in Tests
|
||||
impact: HIGH
|
||||
impactDescription: Timezone bugs are hard to reproduce without consistent test environments
|
||||
tags: testing, timezone, consistency
|
||||
---
|
||||
|
||||
# Timezone Handling in Tests
|
||||
|
||||
## Always Use TZ=UTC
|
||||
|
||||
When running tests in the Cal.diy repository, use the `TZ=UTC` environment variable:
|
||||
|
||||
```bash
|
||||
TZ=UTC yarn test
|
||||
```
|
||||
|
||||
This ensures consistent timezone handling and prevents timezone-related test failures that might occur when tests are run in different environments or by different developers with varying local timezone settings.
|
||||
|
||||
## Why This Matters
|
||||
|
||||
- Tests may pass locally but fail in CI (or vice versa)
|
||||
- Date/time assertions become unpredictable
|
||||
- Debugging timezone issues is time-consuming
|
||||
|
||||
## Test Commands
|
||||
|
||||
```bash
|
||||
# Unit tests
|
||||
TZ=UTC yarn test
|
||||
|
||||
# Specific test file
|
||||
TZ=UTC yarn vitest run path/to/file.test.ts
|
||||
|
||||
# E2E tests (already configured in yarn e2e)
|
||||
PLAYWRIGHT_HEADLESS=1 yarn e2e
|
||||
```
|
||||
@@ -1 +0,0 @@
|
||||
../agents/skills
|
||||
@@ -0,0 +1,139 @@
|
||||
---
|
||||
name: calcom-api
|
||||
description: Interact with the Cal.diy API v2 to manage scheduling, bookings, event types, availability, and calendars. Use this skill when building integrations that need to create or manage bookings, check availability, configure event types, or sync calendars with Cal.diy's scheduling infrastructure.
|
||||
env:
|
||||
CAL_API_KEY:
|
||||
description: "Cal.diy API key (prefixed with cal_live_ or cal_test_). Required for all API requests."
|
||||
required: true
|
||||
CAL_CLIENT_ID:
|
||||
description: "OAuth client ID for platform integrations managing users on behalf of others. Sent as x-cal-client-id header."
|
||||
required: false
|
||||
CAL_SECRET_KEY:
|
||||
description: "OAuth client secret for platform integrations. Sent as x-cal-secret-key header."
|
||||
required: false
|
||||
CAL_WEBHOOK_SECRET:
|
||||
description: "Secret used to verify webhook payload signatures via X-Cal-Signature-256 header."
|
||||
required: false
|
||||
---
|
||||
|
||||
# Cal.diy API v2
|
||||
|
||||
This skill provides guidance for AI agents to interact with the Cal.diy API v2, enabling scheduling automation, booking management, and calendar integrations.
|
||||
|
||||
## Base URL
|
||||
|
||||
All API requests should be made to:
|
||||
```
|
||||
https://api.cal.com/v2
|
||||
```
|
||||
|
||||
## Required Credentials
|
||||
|
||||
| Environment Variable | Required | Description |
|
||||
|---------------------|----------|-------------|
|
||||
| `CAL_API_KEY` | Yes | Cal.diy API key (prefixed with `cal_live_` or `cal_test_`). Used as Bearer token for all API requests. Generate from Settings > Developer > API Keys. |
|
||||
| `CAL_CLIENT_ID` | No | OAuth client ID for platform integrations that manage users on behalf of others. Sent as `x-cal-client-id` header. |
|
||||
| `CAL_SECRET_KEY` | No | OAuth client secret for platform integrations. Sent as `x-cal-secret-key` header. |
|
||||
| `CAL_WEBHOOK_SECRET` | No | Secret for verifying webhook payload signatures via the `X-Cal-Signature-256` header. |
|
||||
|
||||
## Authentication
|
||||
|
||||
All API requests require authentication via Bearer token:
|
||||
|
||||
```
|
||||
Authorization: Bearer cal_<your_api_key>
|
||||
```
|
||||
|
||||
For detailed authentication methods including OAuth/Platform authentication, see `references/authentication.md`.
|
||||
|
||||
## Core Concepts
|
||||
|
||||
**Event Types** define bookable meeting configurations (duration, location, availability rules). Each event type has a unique slug used in booking URLs.
|
||||
|
||||
**Bookings** are confirmed appointments created when someone books an event type. Each booking has a unique UID for identification.
|
||||
|
||||
**Schedules** define when a user is available for bookings. Users can have multiple schedules with different working hours.
|
||||
|
||||
**Slots** represent available time windows that can be booked based on event type configuration and user availability.
|
||||
|
||||
## Reference Documentation
|
||||
|
||||
This skill includes detailed API reference documentation for each domain:
|
||||
|
||||
| Reference | Description |
|
||||
|-----------|-------------|
|
||||
| `references/authentication.md` | API key and OAuth authentication, rate limiting, security best practices |
|
||||
| `references/bookings.md` | Create, list, cancel, reschedule bookings |
|
||||
| `references/event-types.md` | Configure bookable meeting types |
|
||||
| `references/schedules.md` | Manage user availability schedules |
|
||||
| `references/slots-availability.md` | Query available time slots |
|
||||
| `references/calendars.md` | Calendar connections and busy times |
|
||||
| `references/webhooks.md` | Real-time event notifications |
|
||||
|
||||
## Quick Start
|
||||
|
||||
### 1. Check Available Slots
|
||||
|
||||
Before creating a booking, check available time slots:
|
||||
|
||||
```http
|
||||
GET /v2/slots?startTime=2024-01-15T00:00:00Z&endTime=2024-01-22T00:00:00Z&eventTypeId=123
|
||||
```
|
||||
|
||||
See `references/slots-availability.md` for full details.
|
||||
|
||||
### 2. Create a Booking
|
||||
|
||||
```http
|
||||
POST /v2/bookings
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"start": "2024-01-15T10:00:00Z",
|
||||
"eventTypeId": 123,
|
||||
"attendee": {
|
||||
"name": "John Doe",
|
||||
"email": "john@example.com",
|
||||
"timeZone": "America/New_York"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
See `references/bookings.md` for all booking operations.
|
||||
|
||||
### 3. Set Up Webhooks
|
||||
|
||||
Receive real-time notifications for booking events:
|
||||
|
||||
```http
|
||||
POST /v2/webhooks
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"subscriberUrl": "https://your-app.com/webhook",
|
||||
"triggers": ["BOOKING_CREATED", "BOOKING_CANCELLED"]
|
||||
}
|
||||
```
|
||||
|
||||
See `references/webhooks.md` for available triggers and payload formats.
|
||||
|
||||
## Common Workflows
|
||||
|
||||
**Book a meeting**: Check slots -> Create booking -> Store booking UID
|
||||
|
||||
**Reschedule**: Get new slots -> POST /v2/bookings/{uid}/reschedule
|
||||
|
||||
**Cancel**: POST /v2/bookings/{uid}/cancel with optional reason
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. Always check slot availability before creating bookings
|
||||
2. Store booking UIDs for future operations (cancel, reschedule)
|
||||
3. Use ISO 8601 format for all timestamps
|
||||
4. Implement webhook handlers for real-time updates
|
||||
5. Handle rate limiting with exponential backoff
|
||||
|
||||
## Additional Resources
|
||||
|
||||
- [Full API Reference](https://cal.com/docs/api-reference/v2)
|
||||
- [OpenAPI Specification](https://api.cal.com/v2/docs)
|
||||
@@ -0,0 +1,406 @@
|
||||
# Authentication API Reference
|
||||
|
||||
Detailed documentation for authentication methods in the Cal.diy API v2.
|
||||
|
||||
## Authentication Methods
|
||||
|
||||
Cal.diy API v2 supports two authentication methods:
|
||||
|
||||
1. **API Key Authentication** - For direct API access
|
||||
2. **OAuth/Platform Authentication** - For platform integrations managing users on behalf of others
|
||||
|
||||
## API Key Authentication
|
||||
|
||||
The primary authentication method for most API consumers.
|
||||
|
||||
### Obtaining an API Key
|
||||
|
||||
1. Log in to your Cal.diy account
|
||||
2. Navigate to Settings > Developer > API Keys
|
||||
3. Click "Create new API key"
|
||||
4. Copy and securely store the generated key
|
||||
|
||||
### Using API Keys
|
||||
|
||||
Include the API key in the `Authorization` header with the `Bearer` prefix:
|
||||
|
||||
```http
|
||||
GET /v2/bookings
|
||||
Authorization: Bearer cal_live_abc123xyz...
|
||||
```
|
||||
|
||||
### API Key Format
|
||||
|
||||
All Cal.diy API keys are prefixed with `cal_`:
|
||||
- `cal_live_...` - Production API keys
|
||||
- `cal_test_...` - Test/sandbox API keys (if available)
|
||||
|
||||
### Example Request
|
||||
|
||||
```bash
|
||||
curl -X GET "https://api.cal.com/v2/bookings" \
|
||||
-H "Authorization: Bearer cal_live_abc123xyz789" \
|
||||
-H "Content-Type: application/json"
|
||||
```
|
||||
|
||||
## Refresh API Key
|
||||
|
||||
Generate a new API key and invalidate the current one:
|
||||
|
||||
```http
|
||||
POST /v2/api-keys/refresh
|
||||
Authorization: Bearer cal_live_current_key
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"expiresAt": "2025-12-31T23:59:59Z"
|
||||
}
|
||||
```
|
||||
|
||||
### Request Body
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| expiresAt | string | No | ISO 8601 expiration date for the new key |
|
||||
|
||||
### Response
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"data": {
|
||||
"apiKey": "cal_live_new_key_xyz..."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Platform Authentication (OAuth)
|
||||
|
||||
For platform customers building integrations that manage multiple users.
|
||||
|
||||
### Headers for Platform Authentication
|
||||
|
||||
Platform customers use additional headers alongside or instead of the Bearer token:
|
||||
|
||||
| Header | Description |
|
||||
|--------|-------------|
|
||||
| `x-cal-client-id` | OAuth client ID |
|
||||
| `x-cal-secret-key` | OAuth client secret key |
|
||||
| `Authorization` | Bearer token (managed user access token) |
|
||||
|
||||
### Example Platform Request
|
||||
|
||||
```bash
|
||||
curl -X GET "https://api.cal.com/v2/bookings" \
|
||||
-H "x-cal-client-id: your_client_id" \
|
||||
-H "x-cal-secret-key: your_secret_key" \
|
||||
-H "Authorization: Bearer managed_user_access_token" \
|
||||
-H "Content-Type: application/json"
|
||||
```
|
||||
|
||||
### When to Use Each Header
|
||||
|
||||
**For endpoints acting on behalf of a managed user:**
|
||||
```http
|
||||
GET /v2/bookings
|
||||
x-cal-client-id: your_client_id
|
||||
x-cal-secret-key: your_secret_key
|
||||
Authorization: Bearer managed_user_access_token
|
||||
```
|
||||
|
||||
**For platform-level operations (managing OAuth clients):**
|
||||
```http
|
||||
GET /v2/oauth-clients
|
||||
Authorization: Bearer cal_live_platform_admin_key
|
||||
```
|
||||
|
||||
## API Versioning
|
||||
|
||||
Many endpoints require a version header:
|
||||
|
||||
```http
|
||||
cal-api-version: 2024-08-13
|
||||
```
|
||||
|
||||
### Example with Version Header
|
||||
|
||||
```bash
|
||||
curl -X POST "https://api.cal.com/v2/bookings" \
|
||||
-H "Authorization: Bearer cal_live_abc123" \
|
||||
-H "cal-api-version: 2024-08-13" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"start": "2024-01-15T10:00:00Z", "eventTypeId": 123, ...}'
|
||||
```
|
||||
|
||||
## Authentication Errors
|
||||
|
||||
### 401 Unauthorized
|
||||
|
||||
Returned when authentication fails:
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "error",
|
||||
"error": {
|
||||
"code": "UNAUTHORIZED",
|
||||
"message": "Invalid API key"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Common causes:
|
||||
- Missing `Authorization` header
|
||||
- Invalid or expired API key
|
||||
- API key without `cal_` prefix
|
||||
- Incorrect Bearer token format
|
||||
|
||||
### 403 Forbidden
|
||||
|
||||
Returned when authenticated but lacking permissions:
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "error",
|
||||
"error": {
|
||||
"code": "FORBIDDEN",
|
||||
"message": "You do not have permission to access this resource"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Common causes:
|
||||
- Accessing another user's resources
|
||||
- Missing required scopes for platform tokens
|
||||
- Organization/team permission restrictions
|
||||
|
||||
## Security Best Practices
|
||||
|
||||
1. **Never expose API keys in client-side code**: API keys should only be used in server-side applications
|
||||
|
||||
2. **Use environment variables**: Store API keys in environment variables, not in code
|
||||
```bash
|
||||
export CAL_API_KEY="cal_live_abc123..."
|
||||
```
|
||||
|
||||
3. **Rotate keys regularly**: Use the refresh endpoint to rotate keys periodically
|
||||
|
||||
4. **Use minimal permissions**: Request only the scopes/permissions your application needs
|
||||
|
||||
5. **Monitor API usage**: Check your Cal.diy dashboard for unusual activity
|
||||
|
||||
6. **Secure transmission**: Always use HTTPS for API requests
|
||||
|
||||
7. **Handle keys securely in logs**: Never log full API keys - redact sensitive portions
|
||||
|
||||
## Rate Limiting
|
||||
|
||||
API requests are rate limited. When exceeded, you'll receive:
|
||||
|
||||
```http
|
||||
HTTP/1.1 429 Too Many Requests
|
||||
Retry-After: 60
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "error",
|
||||
"error": {
|
||||
"code": "RATE_LIMITED",
|
||||
"message": "Too many requests. Please retry after 60 seconds."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Rate Limit Headers
|
||||
|
||||
| Header | Description |
|
||||
|--------|-------------|
|
||||
| `X-RateLimit-Limit` | Maximum requests per window |
|
||||
| `X-RateLimit-Remaining` | Remaining requests in current window |
|
||||
| `X-RateLimit-Reset` | Unix timestamp when the window resets |
|
||||
| `Retry-After` | Seconds to wait before retrying (on 429) |
|
||||
|
||||
## Testing Authentication
|
||||
|
||||
Verify your API key is working:
|
||||
|
||||
```bash
|
||||
curl -X GET "https://api.cal.com/v2/me" \
|
||||
-H "Authorization: Bearer cal_live_your_api_key" \
|
||||
-H "Content-Type: application/json"
|
||||
```
|
||||
|
||||
### Expected Response
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"data": {
|
||||
"id": 12345,
|
||||
"email": "user@example.com",
|
||||
"username": "johndoe",
|
||||
"name": "John Doe",
|
||||
"timeZone": "America/New_York"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Common Authentication Patterns
|
||||
|
||||
### Server-Side Integration
|
||||
|
||||
```javascript
|
||||
const CAL_API_KEY = process.env.CAL_API_KEY;
|
||||
|
||||
async function getBookings() {
|
||||
const response = await fetch('https://api.cal.com/v2/bookings', {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${CAL_API_KEY}`,
|
||||
'Content-Type': 'application/json',
|
||||
'cal-api-version': '2024-08-13'
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 401) {
|
||||
throw new Error('Invalid API key');
|
||||
}
|
||||
throw new Error(`API error: ${response.status}`);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
```
|
||||
|
||||
### Handling Token Refresh
|
||||
|
||||
```javascript
|
||||
async function makeAuthenticatedRequest(url, options = {}) {
|
||||
let response = await fetch(url, {
|
||||
...options,
|
||||
headers: {
|
||||
...options.headers,
|
||||
'Authorization': `Bearer ${apiKey}`
|
||||
}
|
||||
});
|
||||
|
||||
if (response.status === 401) {
|
||||
// Refresh the API key
|
||||
const refreshResponse = await fetch('https://api.cal.com/v2/api-keys/refresh', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${apiKey}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({})
|
||||
});
|
||||
|
||||
if (refreshResponse.ok) {
|
||||
const { data } = await refreshResponse.json();
|
||||
apiKey = data.apiKey;
|
||||
// Retry original request with new key
|
||||
response = await fetch(url, {
|
||||
...options,
|
||||
headers: {
|
||||
...options.headers,
|
||||
'Authorization': `Bearer ${apiKey}`
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
The API returns standard HTTP status codes:
|
||||
|
||||
| Status Code | Description |
|
||||
|-------------|-------------|
|
||||
| 200 | Success |
|
||||
| 201 | Created |
|
||||
| 400 | Bad Request (invalid parameters) |
|
||||
| 401 | Unauthorized (invalid or missing API key) |
|
||||
| 403 | Forbidden (insufficient permissions) |
|
||||
| 404 | Not Found |
|
||||
| 422 | Unprocessable Entity (validation error) |
|
||||
| 429 | Too Many Requests (rate limited) |
|
||||
| 500 | Internal Server Error |
|
||||
|
||||
### Error Response Format
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "error",
|
||||
"error": {
|
||||
"code": "ERROR_CODE",
|
||||
"message": "Human-readable error message"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Common Error Codes
|
||||
|
||||
| Code | Description |
|
||||
|------|-------------|
|
||||
| UNAUTHORIZED | Invalid or missing authentication |
|
||||
| FORBIDDEN | Insufficient permissions |
|
||||
| NOT_FOUND | Resource not found |
|
||||
| VALIDATION_ERROR | Invalid request parameters |
|
||||
| RATE_LIMITED | Too many requests |
|
||||
|
||||
## Pagination
|
||||
|
||||
List endpoints support pagination via `take` and `skip` parameters:
|
||||
|
||||
| Parameter | Type | Default | Max | Description |
|
||||
|-----------|------|---------|-----|-------------|
|
||||
| take | number | 10 | 250 | Number of items to return |
|
||||
| skip | number | 0 | - | Number of items to skip |
|
||||
|
||||
### Example
|
||||
|
||||
```http
|
||||
GET /v2/bookings?take=20&skip=40
|
||||
```
|
||||
|
||||
This returns items 41-60 (skipping the first 40, taking 20).
|
||||
|
||||
### Pagination Response
|
||||
|
||||
Some endpoints include pagination metadata:
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"data": [...],
|
||||
"pagination": {
|
||||
"total": 150,
|
||||
"take": 20,
|
||||
"skip": 40
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "Invalid API key" Error
|
||||
|
||||
1. Verify the key starts with `cal_`
|
||||
2. Check for extra whitespace or characters
|
||||
3. Ensure the key hasn't been revoked or expired
|
||||
4. Confirm you're using the correct environment (production vs test)
|
||||
|
||||
### "Missing Authorization header" Error
|
||||
|
||||
1. Ensure the header name is exactly `Authorization`
|
||||
2. Include the `Bearer ` prefix (with space)
|
||||
3. Check for typos in the header name
|
||||
|
||||
### Platform Authentication Issues
|
||||
|
||||
1. Verify both `x-cal-client-id` and `x-cal-secret-key` are provided
|
||||
2. Ensure the managed user access token is valid
|
||||
3. Check that the OAuth client has the required permissions
|
||||
@@ -0,0 +1,323 @@
|
||||
# Bookings API Reference
|
||||
|
||||
Detailed documentation for booking-related endpoints in the Cal.diy API v2.
|
||||
|
||||
## Endpoints Overview
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| GET | /v2/bookings | List bookings |
|
||||
| POST | /v2/bookings | Create a booking |
|
||||
| GET | /v2/bookings/{bookingUid} | Get a booking |
|
||||
| POST | /v2/bookings/{bookingUid}/cancel | Cancel a booking |
|
||||
| POST | /v2/bookings/{bookingUid}/reschedule | Reschedule a booking |
|
||||
| POST | /v2/bookings/{bookingUid}/confirm | Confirm a pending booking |
|
||||
| POST | /v2/bookings/{bookingUid}/decline | Decline a booking |
|
||||
| PATCH | /v2/bookings/{bookingUid}/location | Update booking location |
|
||||
| POST | /v2/bookings/{bookingUid}/mark-absent | Mark attendee as no-show |
|
||||
| POST | /v2/bookings/{bookingUid}/reassign | Reassign booking to another host |
|
||||
| GET | /v2/bookings/{bookingUid}/references | Get booking references |
|
||||
|
||||
## List Bookings
|
||||
|
||||
```http
|
||||
GET /v2/bookings
|
||||
```
|
||||
|
||||
### Query Parameters
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|------|----------|-------------|
|
||||
| status | string | No | Filter by status: `upcoming`, `recurring`, `past`, `cancelled`, `unconfirmed` |
|
||||
| attendeeEmail | string | No | Filter by attendee email |
|
||||
| attendeeName | string | No | Filter by attendee name |
|
||||
| eventTypeId | number | No | Filter by event type ID |
|
||||
| eventTypeIds | string | No | Comma-separated event type IDs |
|
||||
| teamsIds | string | No | Comma-separated team IDs |
|
||||
| afterStart | string | No | Filter bookings starting after this ISO 8601 date |
|
||||
| beforeEnd | string | No | Filter bookings ending before this ISO 8601 date |
|
||||
| sortStart | string | No | Sort by start time: `asc` or `desc` |
|
||||
| sortEnd | string | No | Sort by end time: `asc` or `desc` |
|
||||
| sortCreated | string | No | Sort by creation time: `asc` or `desc` |
|
||||
| take | number | No | Number of results (default: 10, max: 250) |
|
||||
| skip | number | No | Pagination offset |
|
||||
|
||||
### Response
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"data": [
|
||||
{
|
||||
"id": 12345,
|
||||
"uid": "abc123def456",
|
||||
"title": "30 Minute Meeting",
|
||||
"description": "Discussion about project",
|
||||
"start": "2024-01-15T10:00:00.000Z",
|
||||
"end": "2024-01-15T10:30:00.000Z",
|
||||
"status": "accepted",
|
||||
"eventTypeId": 123,
|
||||
"attendees": [
|
||||
{
|
||||
"name": "John Doe",
|
||||
"email": "john@example.com",
|
||||
"timeZone": "America/New_York"
|
||||
}
|
||||
],
|
||||
"hosts": [
|
||||
{
|
||||
"id": 456,
|
||||
"name": "Jane Smith",
|
||||
"email": "jane@company.com"
|
||||
}
|
||||
],
|
||||
"location": "https://cal.com/video/abc123",
|
||||
"meetingUrl": "https://cal.com/video/abc123",
|
||||
"metadata": {},
|
||||
"createdAt": "2024-01-10T08:00:00.000Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Create a Booking
|
||||
|
||||
```http
|
||||
POST /v2/bookings
|
||||
```
|
||||
|
||||
### Request Body
|
||||
|
||||
```json
|
||||
{
|
||||
"start": "2024-01-15T10:00:00Z",
|
||||
"eventTypeId": 123,
|
||||
"attendee": {
|
||||
"name": "John Doe",
|
||||
"email": "john@example.com",
|
||||
"timeZone": "America/New_York",
|
||||
"language": "en"
|
||||
},
|
||||
"guests": ["guest1@example.com", "guest2@example.com"],
|
||||
"meetingUrl": "https://cal.com/team/meeting",
|
||||
"metadata": {
|
||||
"customField": "value"
|
||||
},
|
||||
"bookingFieldsResponses": {
|
||||
"notes": "Please prepare the quarterly report"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Required Fields
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| start | string | ISO 8601 booking start time |
|
||||
| eventTypeId | number | ID of the event type to book |
|
||||
| attendee.name | string | Attendee's full name |
|
||||
| attendee.email | string | Attendee's email address |
|
||||
| attendee.timeZone | string | Attendee's timezone (IANA format) |
|
||||
|
||||
### Optional Fields
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| attendee.language | string | Attendee's preferred language |
|
||||
| guests | array | Additional guest email addresses |
|
||||
| meetingUrl | string | Custom meeting URL |
|
||||
| metadata | object | Custom metadata |
|
||||
| bookingFieldsResponses | object | Responses to custom booking fields |
|
||||
|
||||
### Response
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"data": {
|
||||
"id": 12345,
|
||||
"uid": "abc123def456",
|
||||
"title": "30 Minute Meeting",
|
||||
"start": "2024-01-15T10:00:00.000Z",
|
||||
"end": "2024-01-15T10:30:00.000Z",
|
||||
"status": "accepted",
|
||||
"eventTypeId": 123,
|
||||
"attendees": [...],
|
||||
"hosts": [...],
|
||||
"location": "https://cal.com/video/abc123"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Get a Booking
|
||||
|
||||
```http
|
||||
GET /v2/bookings/{bookingUid}
|
||||
```
|
||||
|
||||
### Path Parameters
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| bookingUid | string | Unique booking identifier |
|
||||
|
||||
### Response
|
||||
|
||||
Returns the full booking object with all details.
|
||||
|
||||
## Cancel a Booking
|
||||
|
||||
```http
|
||||
POST /v2/bookings/{bookingUid}/cancel
|
||||
```
|
||||
|
||||
### Request Body
|
||||
|
||||
```json
|
||||
{
|
||||
"cancellationReason": "Schedule conflict"
|
||||
}
|
||||
```
|
||||
|
||||
### Fields
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| cancellationReason | string | No | Reason for cancellation |
|
||||
|
||||
## Reschedule a Booking
|
||||
|
||||
```http
|
||||
POST /v2/bookings/{bookingUid}/reschedule
|
||||
```
|
||||
|
||||
### Request Body
|
||||
|
||||
```json
|
||||
{
|
||||
"start": "2024-01-16T14:00:00Z",
|
||||
"reschedulingReason": "Conflict with another meeting"
|
||||
}
|
||||
```
|
||||
|
||||
### Fields
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| start | string | Yes | New booking start time (ISO 8601) |
|
||||
| reschedulingReason | string | No | Reason for rescheduling |
|
||||
|
||||
## Confirm a Booking
|
||||
|
||||
For event types that require confirmation:
|
||||
|
||||
```http
|
||||
POST /v2/bookings/{bookingUid}/confirm
|
||||
```
|
||||
|
||||
## Decline a Booking
|
||||
|
||||
```http
|
||||
POST /v2/bookings/{bookingUid}/decline
|
||||
```
|
||||
|
||||
### Request Body
|
||||
|
||||
```json
|
||||
{
|
||||
"reason": "Not available at this time"
|
||||
}
|
||||
```
|
||||
|
||||
## Update Booking Location
|
||||
|
||||
```http
|
||||
PATCH /v2/bookings/{bookingUid}/location
|
||||
```
|
||||
|
||||
### Request Body
|
||||
|
||||
```json
|
||||
{
|
||||
"location": "https://zoom.us/j/123456789"
|
||||
}
|
||||
```
|
||||
|
||||
## Mark Attendee as No-Show
|
||||
|
||||
```http
|
||||
POST /v2/bookings/{bookingUid}/mark-absent
|
||||
```
|
||||
|
||||
### Request Body
|
||||
|
||||
```json
|
||||
{
|
||||
"attendeeEmail": "john@example.com",
|
||||
"noShow": true
|
||||
}
|
||||
```
|
||||
|
||||
## Reassign Booking
|
||||
|
||||
Reassign a booking to a different host:
|
||||
|
||||
```http
|
||||
POST /v2/bookings/{bookingUid}/reassign
|
||||
```
|
||||
|
||||
Or to a specific user:
|
||||
|
||||
```http
|
||||
POST /v2/bookings/{bookingUid}/reassign/{userId}
|
||||
```
|
||||
|
||||
## Get Booking References
|
||||
|
||||
Get external references (calendar events, video meetings) for a booking:
|
||||
|
||||
```http
|
||||
GET /v2/bookings/{bookingUid}/references
|
||||
```
|
||||
|
||||
### Response
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"data": [
|
||||
{
|
||||
"type": "google_calendar",
|
||||
"uid": "calendar-event-id",
|
||||
"meetingUrl": "https://meet.google.com/abc-defg-hij"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Booking Statuses
|
||||
|
||||
| Status | Description |
|
||||
|--------|-------------|
|
||||
| accepted | Booking is confirmed |
|
||||
| pending | Awaiting confirmation |
|
||||
| cancelled | Booking was cancelled |
|
||||
| rejected | Booking was declined |
|
||||
|
||||
## Common Use Cases
|
||||
|
||||
### Book a Meeting
|
||||
|
||||
1. Get available slots: `GET /v2/slots?eventTypeId=123&startTime=...&endTime=...`
|
||||
2. Create booking: `POST /v2/bookings` with selected slot
|
||||
3. Store the booking UID for future operations
|
||||
|
||||
### Reschedule Flow
|
||||
|
||||
1. Get new available slots: `GET /v2/slots?eventTypeId=123&startTime=...&endTime=...`
|
||||
2. Reschedule: `POST /v2/bookings/{uid}/reschedule` with new start time
|
||||
|
||||
### Cancel with Notification
|
||||
|
||||
1. Cancel: `POST /v2/bookings/{uid}/cancel` with reason
|
||||
2. Attendees automatically receive cancellation emails
|
||||
@@ -0,0 +1,327 @@
|
||||
# Calendars API Reference
|
||||
|
||||
Detailed documentation for calendar integration endpoints in the Cal.diy API v2.
|
||||
|
||||
## Endpoints Overview
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| GET | /v2/calendars | List connected calendars |
|
||||
| GET | /v2/calendars/busy-times | Get busy times |
|
||||
| GET | /v2/calendars/{calendar}/check | Check calendar connection |
|
||||
| POST | /v2/calendars/{calendar}/connect | Connect a calendar |
|
||||
| DELETE | /v2/calendars/{calendar}/disconnect | Disconnect a calendar |
|
||||
| GET | /v2/calendars/{calendar}/credentials | Get calendar credentials |
|
||||
| GET | /v2/destination-calendars | List destination calendars |
|
||||
| GET | /v2/selected-calendars | List selected calendars |
|
||||
|
||||
## List Connected Calendars
|
||||
|
||||
```http
|
||||
GET /v2/calendars
|
||||
```
|
||||
|
||||
### Response
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"data": {
|
||||
"connectedCalendars": [
|
||||
{
|
||||
"integration": {
|
||||
"type": "google_calendar",
|
||||
"title": "Google Calendar",
|
||||
"slug": "google-calendar"
|
||||
},
|
||||
"credentialId": 123,
|
||||
"primary": {
|
||||
"externalId": "primary",
|
||||
"name": "john@gmail.com",
|
||||
"email": "john@gmail.com",
|
||||
"isSelected": true,
|
||||
"readOnly": false
|
||||
},
|
||||
"calendars": [
|
||||
{
|
||||
"externalId": "primary",
|
||||
"name": "john@gmail.com",
|
||||
"email": "john@gmail.com",
|
||||
"isSelected": true,
|
||||
"readOnly": false
|
||||
},
|
||||
{
|
||||
"externalId": "calendar-id-2",
|
||||
"name": "Work Calendar",
|
||||
"email": "john@gmail.com",
|
||||
"isSelected": true,
|
||||
"readOnly": false
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"destinationCalendar": {
|
||||
"id": 1,
|
||||
"integration": "google_calendar",
|
||||
"externalId": "primary",
|
||||
"name": "john@gmail.com"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Get Busy Times
|
||||
|
||||
Check busy times from connected calendars to understand availability.
|
||||
|
||||
```http
|
||||
GET /v2/calendars/busy-times
|
||||
```
|
||||
|
||||
### Query Parameters
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|------|----------|-------------|
|
||||
| startTime | string | Yes | ISO 8601 start of date range |
|
||||
| endTime | string | Yes | ISO 8601 end of date range |
|
||||
| loggedInUsersTz | string | No | User's timezone |
|
||||
| credentialId | number | No | Specific calendar credential ID |
|
||||
|
||||
### Example Request
|
||||
|
||||
```http
|
||||
GET /v2/calendars/busy-times?startTime=2024-01-15T00:00:00Z&endTime=2024-01-22T00:00:00Z&loggedInUsersTz=America/New_York
|
||||
```
|
||||
|
||||
### Response
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"data": [
|
||||
{
|
||||
"start": "2024-01-15T10:00:00.000Z",
|
||||
"end": "2024-01-15T11:00:00.000Z",
|
||||
"title": "Team Meeting",
|
||||
"source": "google_calendar"
|
||||
},
|
||||
{
|
||||
"start": "2024-01-16T14:00:00.000Z",
|
||||
"end": "2024-01-16T15:00:00.000Z",
|
||||
"title": "Client Call",
|
||||
"source": "google_calendar"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Calendar Connection
|
||||
|
||||
### Check Calendar Connection
|
||||
|
||||
```http
|
||||
GET /v2/calendars/{calendar}/check
|
||||
```
|
||||
|
||||
### Path Parameters
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| calendar | string | Calendar type (e.g., `google-calendar`, `office365-calendar`) |
|
||||
|
||||
### Connect a Calendar
|
||||
|
||||
```http
|
||||
POST /v2/calendars/{calendar}/connect
|
||||
```
|
||||
|
||||
This initiates the OAuth flow for calendar connection.
|
||||
|
||||
### Disconnect a Calendar
|
||||
|
||||
```http
|
||||
DELETE /v2/calendars/{calendar}/disconnect
|
||||
```
|
||||
|
||||
## Supported Calendar Types
|
||||
|
||||
| Type | Slug | Description |
|
||||
|------|------|-------------|
|
||||
| Google Calendar | google-calendar | Google Workspace calendars |
|
||||
| Microsoft 365 | office365-calendar | Outlook/Microsoft 365 calendars |
|
||||
| Apple Calendar | apple-calendar | iCloud calendars (CalDAV) |
|
||||
| CalDAV | caldav-calendar | Generic CalDAV calendars |
|
||||
|
||||
## Destination Calendars
|
||||
|
||||
The destination calendar is where new bookings are created.
|
||||
|
||||
### List Destination Calendars
|
||||
|
||||
```http
|
||||
GET /v2/destination-calendars
|
||||
```
|
||||
|
||||
### Response
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"data": [
|
||||
{
|
||||
"id": 1,
|
||||
"integration": "google_calendar",
|
||||
"externalId": "primary",
|
||||
"name": "john@gmail.com",
|
||||
"userId": 123,
|
||||
"eventTypeId": null,
|
||||
"credentialId": 456
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Selected Calendars
|
||||
|
||||
Selected calendars are checked for conflicts when calculating availability.
|
||||
|
||||
### List Selected Calendars
|
||||
|
||||
```http
|
||||
GET /v2/selected-calendars
|
||||
```
|
||||
|
||||
### Response
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"data": [
|
||||
{
|
||||
"integration": "google_calendar",
|
||||
"externalId": "primary",
|
||||
"credentialId": 123
|
||||
},
|
||||
{
|
||||
"integration": "google_calendar",
|
||||
"externalId": "work-calendar-id",
|
||||
"credentialId": 123
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## ICS Feed
|
||||
|
||||
### Check ICS Feed
|
||||
|
||||
```http
|
||||
GET /v2/calendars/ics-feed/check
|
||||
```
|
||||
|
||||
### Save ICS Feed
|
||||
|
||||
```http
|
||||
POST /v2/calendars/ics-feed/save
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"url": "https://calendar.example.com/feed.ics"
|
||||
}
|
||||
```
|
||||
|
||||
## Calendar Events
|
||||
|
||||
### Get Calendar Event
|
||||
|
||||
```http
|
||||
GET /v2/calendars/{calendar}/events/{eventUid}
|
||||
```
|
||||
|
||||
### Response
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"data": {
|
||||
"id": "event-id-123",
|
||||
"title": "Meeting",
|
||||
"description": "Discussion",
|
||||
"start": {
|
||||
"time": "2024-01-15T10:00:00.000Z",
|
||||
"timeZone": "America/New_York"
|
||||
},
|
||||
"end": {
|
||||
"time": "2024-01-15T11:00:00.000Z",
|
||||
"timeZone": "America/New_York"
|
||||
},
|
||||
"attendees": [
|
||||
{
|
||||
"email": "attendee@example.com",
|
||||
"name": "Attendee Name",
|
||||
"responseStatus": "accepted"
|
||||
}
|
||||
],
|
||||
"status": "accepted",
|
||||
"source": "google"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Understanding Calendar Integration
|
||||
|
||||
### How Calendars Affect Availability
|
||||
|
||||
1. **Selected Calendars**: Events from selected calendars block availability
|
||||
2. **Destination Calendar**: New bookings are created in this calendar
|
||||
3. **Busy Times**: The API aggregates busy times from all selected calendars
|
||||
|
||||
### Calendar Sync Flow
|
||||
|
||||
```
|
||||
1. User connects calendar (OAuth)
|
||||
POST /v2/calendars/google-calendar/connect
|
||||
|
||||
2. User selects which calendars to check for conflicts
|
||||
(Done via Cal.diy dashboard)
|
||||
|
||||
3. User sets destination calendar for new bookings
|
||||
(Done via Cal.diy dashboard)
|
||||
|
||||
4. When checking slots:
|
||||
- API fetches busy times from all selected calendars
|
||||
- Busy times are excluded from available slots
|
||||
|
||||
5. When booking is created:
|
||||
- Event is created in destination calendar
|
||||
- Confirmation emails sent to attendees
|
||||
```
|
||||
|
||||
### Cal.diy Event Identification
|
||||
|
||||
Cal.diy events in external calendars can be identified by their iCalUID ending with `@Cal.diy` (e.g., `2GBXSdEixretciJfKVmYN8@Cal.diy`).
|
||||
|
||||
## Team Calendar Integration
|
||||
|
||||
For team-level calendar management:
|
||||
|
||||
### Team Conferencing
|
||||
|
||||
```http
|
||||
GET /v2/organizations/{orgId}/teams/{teamId}/conferencing
|
||||
```
|
||||
|
||||
### Connect Team Calendar
|
||||
|
||||
```http
|
||||
POST /v2/organizations/{orgId}/teams/{teamId}/conferencing/{app}/connect
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Check connection status**: Verify calendar is connected before operations
|
||||
2. **Handle OAuth expiry**: Calendar tokens may expire, handle re-authentication
|
||||
3. **Respect rate limits**: Calendar APIs have their own rate limits
|
||||
4. **Cache busy times**: Busy times don't change frequently, cache appropriately
|
||||
5. **Use webhooks**: Subscribe to booking events instead of polling calendars
|
||||
@@ -0,0 +1,439 @@
|
||||
# Event Types API Reference
|
||||
|
||||
Detailed documentation for event type endpoints in the Cal.diy API v2.
|
||||
|
||||
## Endpoints Overview
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| GET | /v2/event-types | List event types |
|
||||
| POST | /v2/event-types | Create an event type |
|
||||
| GET | /v2/event-types/{eventTypeId} | Get an event type |
|
||||
| PATCH | /v2/event-types/{eventTypeId} | Update an event type |
|
||||
| DELETE | /v2/event-types/{eventTypeId} | Delete an event type |
|
||||
|
||||
## List Event Types
|
||||
|
||||
```http
|
||||
GET /v2/event-types
|
||||
```
|
||||
|
||||
### Query Parameters
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|------|----------|-------------|
|
||||
| take | number | No | Number of results (default: 10, max: 250) |
|
||||
| skip | number | No | Pagination offset |
|
||||
|
||||
### Response
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"data": [
|
||||
{
|
||||
"id": 123,
|
||||
"title": "30 Minute Meeting",
|
||||
"slug": "30min",
|
||||
"description": "A quick 30 minute call",
|
||||
"lengthInMinutes": 30,
|
||||
"locations": [
|
||||
{
|
||||
"type": "integration",
|
||||
"integration": "cal-video"
|
||||
}
|
||||
],
|
||||
"bookingFields": [],
|
||||
"disableGuests": false,
|
||||
"slotInterval": null,
|
||||
"minimumBookingNotice": 120,
|
||||
"beforeEventBuffer": 0,
|
||||
"afterEventBuffer": 0,
|
||||
"schedulingType": null,
|
||||
"metadata": {},
|
||||
"requiresConfirmation": false,
|
||||
"price": 0,
|
||||
"currency": "usd",
|
||||
"hidden": false
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Create an Event Type
|
||||
|
||||
```http
|
||||
POST /v2/event-types
|
||||
```
|
||||
|
||||
### Request Body
|
||||
|
||||
```json
|
||||
{
|
||||
"title": "30 Minute Meeting",
|
||||
"slug": "30min",
|
||||
"description": "A quick 30 minute call to discuss your needs",
|
||||
"lengthInMinutes": 30,
|
||||
"locations": [
|
||||
{
|
||||
"type": "integration",
|
||||
"integration": "cal-video"
|
||||
}
|
||||
],
|
||||
"bookingFields": [
|
||||
{
|
||||
"type": "textarea",
|
||||
"name": "notes",
|
||||
"label": "Additional Notes",
|
||||
"required": false,
|
||||
"placeholder": "Any additional information..."
|
||||
}
|
||||
],
|
||||
"disableGuests": false,
|
||||
"slotInterval": 15,
|
||||
"minimumBookingNotice": 120,
|
||||
"beforeEventBuffer": 5,
|
||||
"afterEventBuffer": 5,
|
||||
"scheduleId": 1,
|
||||
"requiresConfirmation": false,
|
||||
"hidden": false
|
||||
}
|
||||
```
|
||||
|
||||
### Required Fields
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| title | string | Display name of the event type |
|
||||
| slug | string | URL-friendly identifier |
|
||||
| lengthInMinutes | number | Duration of the event |
|
||||
|
||||
### Optional Fields
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| description | string | Description shown on booking page |
|
||||
| locations | array | Meeting location options |
|
||||
| bookingFields | array | Custom form fields |
|
||||
| disableGuests | boolean | Prevent attendees from adding guests |
|
||||
| slotInterval | number | Minutes between available slots |
|
||||
| minimumBookingNotice | number | Minimum minutes before booking |
|
||||
| beforeEventBuffer | number | Buffer time before event (minutes) |
|
||||
| afterEventBuffer | number | Buffer time after event (minutes) |
|
||||
| scheduleId | number | ID of schedule to use |
|
||||
| requiresConfirmation | boolean | Require host confirmation |
|
||||
| hidden | boolean | Hide from public profile |
|
||||
|
||||
## Location Types
|
||||
|
||||
### Cal Video (Built-in)
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "integration",
|
||||
"integration": "cal-video"
|
||||
}
|
||||
```
|
||||
|
||||
### Zoom
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "integration",
|
||||
"integration": "zoom"
|
||||
}
|
||||
```
|
||||
|
||||
### Google Meet
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "integration",
|
||||
"integration": "google-meet"
|
||||
}
|
||||
```
|
||||
|
||||
### Microsoft Teams
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "integration",
|
||||
"integration": "msteams"
|
||||
}
|
||||
```
|
||||
|
||||
### In-Person
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "address",
|
||||
"address": "123 Main St, City, Country"
|
||||
}
|
||||
```
|
||||
|
||||
### Phone Call (Host Calls)
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "userPhone"
|
||||
}
|
||||
```
|
||||
|
||||
### Phone Call (Attendee Provides)
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "attendeePhone"
|
||||
}
|
||||
```
|
||||
|
||||
### Custom Link
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "link",
|
||||
"link": "https://custom-meeting.com/room"
|
||||
}
|
||||
```
|
||||
|
||||
## Booking Fields
|
||||
|
||||
Custom form fields for collecting information from attendees.
|
||||
|
||||
### Text Field
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "text",
|
||||
"name": "company",
|
||||
"label": "Company Name",
|
||||
"required": true,
|
||||
"placeholder": "Enter your company name"
|
||||
}
|
||||
```
|
||||
|
||||
### Textarea
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "textarea",
|
||||
"name": "notes",
|
||||
"label": "Additional Notes",
|
||||
"required": false
|
||||
}
|
||||
```
|
||||
|
||||
### Select (Dropdown)
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "select",
|
||||
"name": "topic",
|
||||
"label": "Meeting Topic",
|
||||
"required": true,
|
||||
"options": [
|
||||
{ "value": "sales", "label": "Sales Inquiry" },
|
||||
{ "value": "support", "label": "Support" },
|
||||
{ "value": "other", "label": "Other" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Radio Buttons
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "radio",
|
||||
"name": "preference",
|
||||
"label": "Preferred Contact Method",
|
||||
"required": true,
|
||||
"options": [
|
||||
{ "value": "email", "label": "Email" },
|
||||
{ "value": "phone", "label": "Phone" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Checkbox
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "checkbox",
|
||||
"name": "terms",
|
||||
"label": "I agree to the terms",
|
||||
"required": true
|
||||
}
|
||||
```
|
||||
|
||||
### Phone Number
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "phone",
|
||||
"name": "phone",
|
||||
"label": "Phone Number",
|
||||
"required": false
|
||||
}
|
||||
```
|
||||
|
||||
## Get an Event Type
|
||||
|
||||
```http
|
||||
GET /v2/event-types/{eventTypeId}
|
||||
```
|
||||
|
||||
### Path Parameters
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| eventTypeId | number | Event type ID |
|
||||
|
||||
## Update an Event Type
|
||||
|
||||
```http
|
||||
PATCH /v2/event-types/{eventTypeId}
|
||||
```
|
||||
|
||||
### Request Body
|
||||
|
||||
Only include fields you want to update:
|
||||
|
||||
```json
|
||||
{
|
||||
"title": "Updated Meeting Title",
|
||||
"lengthInMinutes": 45,
|
||||
"hidden": false
|
||||
}
|
||||
```
|
||||
|
||||
## Delete an Event Type
|
||||
|
||||
```http
|
||||
DELETE /v2/event-types/{eventTypeId}
|
||||
```
|
||||
|
||||
## Team Event Types
|
||||
|
||||
For team event types, use the team-scoped endpoints:
|
||||
|
||||
### List Team Event Types
|
||||
|
||||
```http
|
||||
GET /v2/teams/{teamId}/event-types
|
||||
```
|
||||
|
||||
### Create Team Event Type
|
||||
|
||||
```http
|
||||
POST /v2/teams/{teamId}/event-types
|
||||
```
|
||||
|
||||
Additional fields for team event types:
|
||||
|
||||
```json
|
||||
{
|
||||
"title": "Team Meeting",
|
||||
"slug": "team-meeting",
|
||||
"lengthInMinutes": 30,
|
||||
"schedulingType": "ROUND_ROBIN",
|
||||
"hosts": [
|
||||
{ "userId": 1, "isFixed": false },
|
||||
{ "userId": 2, "isFixed": false }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Scheduling Types
|
||||
|
||||
| Type | Description |
|
||||
|------|-------------|
|
||||
| ROUND_ROBIN | Distributes bookings among team members |
|
||||
| COLLECTIVE | All team members must attend |
|
||||
| MANAGED | Parent event type that creates child event types |
|
||||
|
||||
## Private Links
|
||||
|
||||
Create private booking links that bypass public visibility:
|
||||
|
||||
### List Private Links
|
||||
|
||||
```http
|
||||
GET /v2/event-types/{eventTypeId}/private-links
|
||||
```
|
||||
|
||||
### Create Private Link
|
||||
|
||||
```http
|
||||
POST /v2/event-types/{eventTypeId}/private-links
|
||||
```
|
||||
|
||||
## Organization Event Types
|
||||
|
||||
For organization-level event type management:
|
||||
|
||||
### List Organization Teams
|
||||
|
||||
```http
|
||||
GET /v2/organizations/{orgId}/teams
|
||||
```
|
||||
|
||||
### Response
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"data": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "Sales Team",
|
||||
"slug": "sales",
|
||||
"parentId": null,
|
||||
"isOrganization": false
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Get Team Event Types (Organization Scope)
|
||||
|
||||
```http
|
||||
GET /v2/organizations/{orgId}/teams/{teamId}/event-types
|
||||
```
|
||||
|
||||
### Create Team Event Type (Organization Scope)
|
||||
|
||||
```http
|
||||
POST /v2/organizations/{orgId}/teams/{teamId}/event-types
|
||||
```
|
||||
|
||||
Team event types support different scheduling modes:
|
||||
|
||||
| Mode | Description |
|
||||
|------|-------------|
|
||||
| COLLECTIVE | All team members must attend the meeting |
|
||||
| ROUND_ROBIN | Distributes bookings among team members based on availability and priority |
|
||||
| MANAGED | Parent event type that creates child event types for team members |
|
||||
|
||||
## Event Type Webhooks
|
||||
|
||||
Configure webhooks specific to an event type:
|
||||
|
||||
### List Event Type Webhooks
|
||||
|
||||
```http
|
||||
GET /v2/event-types/{eventTypeId}/webhooks
|
||||
```
|
||||
|
||||
### Create Event Type Webhook
|
||||
|
||||
```http
|
||||
POST /v2/event-types/{eventTypeId}/webhooks
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"subscriberUrl": "https://your-app.com/webhook",
|
||||
"triggers": ["BOOKING_CREATED"],
|
||||
"active": true
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,359 @@
|
||||
# Schedules API Reference
|
||||
|
||||
Detailed documentation for schedule management endpoints in the Cal.diy API v2.
|
||||
|
||||
## Endpoints Overview
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| GET | /v2/schedules | List all schedules |
|
||||
| POST | /v2/schedules | Create a schedule |
|
||||
| GET | /v2/schedules/default | Get default schedule |
|
||||
| GET | /v2/schedules/{scheduleId} | Get a schedule |
|
||||
| PATCH | /v2/schedules/{scheduleId} | Update a schedule |
|
||||
| DELETE | /v2/schedules/{scheduleId} | Delete a schedule |
|
||||
|
||||
## Understanding Schedules
|
||||
|
||||
Schedules define when a user is available for bookings. Key concepts:
|
||||
|
||||
- **Working Hours**: Regular weekly availability (e.g., Mon-Fri 9am-5pm)
|
||||
- **Date Overrides**: Exceptions to regular hours (e.g., holiday, special hours)
|
||||
- **Timezone**: The timezone in which availability is defined
|
||||
- **Default Schedule**: The primary schedule used when no specific schedule is assigned
|
||||
|
||||
## List Schedules
|
||||
|
||||
```http
|
||||
GET /v2/schedules
|
||||
```
|
||||
|
||||
### Response
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"data": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "Working Hours",
|
||||
"isDefault": true,
|
||||
"timeZone": "America/New_York",
|
||||
"workingHours": [
|
||||
{
|
||||
"days": [1, 2, 3, 4, 5],
|
||||
"startTime": 540,
|
||||
"endTime": 1020
|
||||
}
|
||||
],
|
||||
"availability": [
|
||||
{
|
||||
"id": 1,
|
||||
"days": [1, 2, 3, 4, 5],
|
||||
"startTime": "1970-01-01T09:00:00.000Z",
|
||||
"endTime": "1970-01-01T17:00:00.000Z"
|
||||
}
|
||||
],
|
||||
"dateOverrides": [],
|
||||
"isManaged": false,
|
||||
"readOnly": false,
|
||||
"isLastSchedule": false
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Create a Schedule
|
||||
|
||||
```http
|
||||
POST /v2/schedules
|
||||
```
|
||||
|
||||
### Request Body
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "Working Hours",
|
||||
"timeZone": "America/New_York",
|
||||
"isDefault": true,
|
||||
"availability": [
|
||||
{
|
||||
"days": [1, 2, 3, 4, 5],
|
||||
"startTime": "09:00",
|
||||
"endTime": "17:00"
|
||||
}
|
||||
],
|
||||
"dateOverrides": [
|
||||
{
|
||||
"date": "2024-12-25",
|
||||
"startTime": null,
|
||||
"endTime": null
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Fields
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| name | string | Yes | Schedule name |
|
||||
| timeZone | string | Yes | IANA timezone identifier |
|
||||
| isDefault | boolean | No | Set as default schedule |
|
||||
| availability | array | Yes | Weekly availability rules |
|
||||
| dateOverrides | array | No | Date-specific overrides |
|
||||
|
||||
### Availability Object
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| days | array | Days of week (0=Sunday, 1=Monday, ..., 6=Saturday) |
|
||||
| startTime | string | Start time in HH:MM format |
|
||||
| endTime | string | End time in HH:MM format |
|
||||
|
||||
### Date Override Object
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| date | string | Date in YYYY-MM-DD format |
|
||||
| startTime | string/null | Start time (null = unavailable) |
|
||||
| endTime | string/null | End time (null = unavailable) |
|
||||
|
||||
## Get Default Schedule
|
||||
|
||||
```http
|
||||
GET /v2/schedules/default
|
||||
```
|
||||
|
||||
Returns the user's default schedule.
|
||||
|
||||
## Get a Schedule
|
||||
|
||||
```http
|
||||
GET /v2/schedules/{scheduleId}
|
||||
```
|
||||
|
||||
### Path Parameters
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| scheduleId | number | Schedule ID |
|
||||
|
||||
## Update a Schedule
|
||||
|
||||
```http
|
||||
PATCH /v2/schedules/{scheduleId}
|
||||
```
|
||||
|
||||
### Request Body
|
||||
|
||||
Only include fields you want to update:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "Updated Schedule Name",
|
||||
"availability": [
|
||||
{
|
||||
"days": [1, 2, 3, 4, 5],
|
||||
"startTime": "08:00",
|
||||
"endTime": "18:00"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Delete a Schedule
|
||||
|
||||
```http
|
||||
DELETE /v2/schedules/{scheduleId}
|
||||
```
|
||||
|
||||
Note: You cannot delete your last schedule. At least one schedule must exist.
|
||||
|
||||
## Common Schedule Patterns
|
||||
|
||||
### Standard Business Hours (Mon-Fri 9-5)
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "Business Hours",
|
||||
"timeZone": "America/New_York",
|
||||
"availability": [
|
||||
{
|
||||
"days": [1, 2, 3, 4, 5],
|
||||
"startTime": "09:00",
|
||||
"endTime": "17:00"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Split Schedule (Morning and Afternoon)
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "Split Hours",
|
||||
"timeZone": "America/New_York",
|
||||
"availability": [
|
||||
{
|
||||
"days": [1, 2, 3, 4, 5],
|
||||
"startTime": "09:00",
|
||||
"endTime": "12:00"
|
||||
},
|
||||
{
|
||||
"days": [1, 2, 3, 4, 5],
|
||||
"startTime": "14:00",
|
||||
"endTime": "18:00"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Different Hours Per Day
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "Variable Hours",
|
||||
"timeZone": "America/New_York",
|
||||
"availability": [
|
||||
{
|
||||
"days": [1, 3, 5],
|
||||
"startTime": "09:00",
|
||||
"endTime": "17:00"
|
||||
},
|
||||
{
|
||||
"days": [2, 4],
|
||||
"startTime": "10:00",
|
||||
"endTime": "19:00"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Weekend Availability
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "Weekend Support",
|
||||
"timeZone": "America/New_York",
|
||||
"availability": [
|
||||
{
|
||||
"days": [0, 6],
|
||||
"startTime": "10:00",
|
||||
"endTime": "14:00"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Holiday Override (Unavailable)
|
||||
|
||||
```json
|
||||
{
|
||||
"dateOverrides": [
|
||||
{
|
||||
"date": "2024-12-25",
|
||||
"startTime": null,
|
||||
"endTime": null
|
||||
},
|
||||
{
|
||||
"date": "2024-01-01",
|
||||
"startTime": null,
|
||||
"endTime": null
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Special Hours Override
|
||||
|
||||
```json
|
||||
{
|
||||
"dateOverrides": [
|
||||
{
|
||||
"date": "2024-12-24",
|
||||
"startTime": "09:00",
|
||||
"endTime": "12:00"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Organization/Team Schedules
|
||||
|
||||
For organization-level schedule management:
|
||||
|
||||
### List Organization Schedules
|
||||
|
||||
```http
|
||||
GET /v2/organizations/{orgId}/schedules
|
||||
```
|
||||
|
||||
### List User Schedules in Organization
|
||||
|
||||
```http
|
||||
GET /v2/organizations/{orgId}/users/{userId}/schedules
|
||||
```
|
||||
|
||||
### Create User Schedule in Organization
|
||||
|
||||
```http
|
||||
POST /v2/organizations/{orgId}/users/{userId}/schedules
|
||||
```
|
||||
|
||||
### Team Member Schedules
|
||||
|
||||
```http
|
||||
GET /v2/organizations/{orgId}/teams/{teamId}/users/{userId}/schedules
|
||||
```
|
||||
|
||||
## Working Hours Format
|
||||
|
||||
The API returns working hours in two formats:
|
||||
|
||||
### Minutes from Midnight
|
||||
|
||||
```json
|
||||
{
|
||||
"workingHours": [
|
||||
{
|
||||
"days": [1, 2, 3, 4, 5],
|
||||
"startTime": 540,
|
||||
"endTime": 1020
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
- `540` = 9:00 AM (9 * 60 minutes)
|
||||
- `1020` = 5:00 PM (17 * 60 minutes)
|
||||
|
||||
### ISO Time Format
|
||||
|
||||
```json
|
||||
{
|
||||
"availability": [
|
||||
{
|
||||
"days": [1, 2, 3, 4, 5],
|
||||
"startTime": "1970-01-01T09:00:00.000Z",
|
||||
"endTime": "1970-01-01T17:00:00.000Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
When creating/updating, use HH:MM format:
|
||||
|
||||
```json
|
||||
{
|
||||
"startTime": "09:00",
|
||||
"endTime": "17:00"
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Always specify timezone**: Schedules are timezone-aware
|
||||
2. **Use date overrides sparingly**: For recurring patterns, create separate schedules
|
||||
3. **Test availability**: After creating a schedule, verify with the slots endpoint
|
||||
4. **Consider buffer times**: Set buffers on event types, not schedules
|
||||
@@ -0,0 +1,255 @@
|
||||
# Slots and Availability API Reference
|
||||
|
||||
Detailed documentation for checking availability and managing slots in the Cal.diy API v2.
|
||||
|
||||
## Endpoints Overview
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| GET | /v2/slots | Get available time slots |
|
||||
| POST | /v2/slots/reservations | Reserve a slot temporarily |
|
||||
| DELETE | /v2/slots/reservations/{uid} | Release a reserved slot |
|
||||
| GET | /v2/calendars/busy-times | Get busy times from calendars |
|
||||
|
||||
## Get Available Slots
|
||||
|
||||
Check available time slots for booking an event type.
|
||||
|
||||
```http
|
||||
GET /v2/slots
|
||||
```
|
||||
|
||||
### Query Parameters
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|------|----------|-------------|
|
||||
| startTime | string | Yes | ISO 8601 start of date range |
|
||||
| endTime | string | Yes | ISO 8601 end of date range |
|
||||
| eventTypeId | number | Conditional | Event type ID (required if no slug) |
|
||||
| eventTypeSlug | string | Conditional | Event type slug (required if no ID) |
|
||||
| usernameList | string | Conditional | Comma-separated usernames for team events |
|
||||
| timeZone | string | No | Timezone for slot display (default: UTC) |
|
||||
| duration | number | No | Override event duration in minutes |
|
||||
| rescheduleUid | string | No | Booking UID if rescheduling |
|
||||
|
||||
### Example Request
|
||||
|
||||
```http
|
||||
GET /v2/slots?startTime=2024-01-15T00:00:00Z&endTime=2024-01-22T00:00:00Z&eventTypeId=123&timeZone=America/New_York
|
||||
```
|
||||
|
||||
### Response
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"data": {
|
||||
"slots": {
|
||||
"2024-01-15": [
|
||||
{
|
||||
"time": "2024-01-15T09:00:00.000Z"
|
||||
},
|
||||
{
|
||||
"time": "2024-01-15T09:30:00.000Z"
|
||||
},
|
||||
{
|
||||
"time": "2024-01-15T10:00:00.000Z"
|
||||
}
|
||||
],
|
||||
"2024-01-16": [
|
||||
{
|
||||
"time": "2024-01-16T09:00:00.000Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Response with Attendees (Seated Events)
|
||||
|
||||
For event types with `seatsPerTimeSlot` configured:
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"data": {
|
||||
"slots": {
|
||||
"2024-01-15": [
|
||||
{
|
||||
"time": "2024-01-15T09:00:00.000Z",
|
||||
"attendees": 3,
|
||||
"seatsAvailable": 7
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Reserve a Slot
|
||||
|
||||
Temporarily reserve a slot while the user completes the booking form. This prevents double-booking.
|
||||
|
||||
```http
|
||||
POST /v2/slots/reservations
|
||||
```
|
||||
|
||||
### Request Body
|
||||
|
||||
```json
|
||||
{
|
||||
"eventTypeId": 123,
|
||||
"slotUtcStartDate": "2024-01-15T09:00:00.000Z",
|
||||
"slotUtcEndDate": "2024-01-15T09:30:00.000Z"
|
||||
}
|
||||
```
|
||||
|
||||
### Response
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"data": {
|
||||
"uid": "reservation-uid-123",
|
||||
"eventTypeId": 123,
|
||||
"slotUtcStartDate": "2024-01-15T09:00:00.000Z",
|
||||
"slotUtcEndDate": "2024-01-15T09:30:00.000Z",
|
||||
"expiresAt": "2024-01-15T08:10:00.000Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Reservations automatically expire after a short period (typically 10 minutes).
|
||||
|
||||
## Release a Reserved Slot
|
||||
|
||||
Release a slot reservation if the user abandons the booking flow.
|
||||
|
||||
```http
|
||||
DELETE /v2/slots/reservations/{uid}
|
||||
```
|
||||
|
||||
### Path Parameters
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| uid | string | Reservation UID |
|
||||
|
||||
## Get Busy Times
|
||||
|
||||
Check busy times from connected calendars.
|
||||
|
||||
```http
|
||||
GET /v2/calendars/busy-times
|
||||
```
|
||||
|
||||
### Query Parameters
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|------|----------|-------------|
|
||||
| startTime | string | Yes | ISO 8601 start of date range |
|
||||
| endTime | string | Yes | ISO 8601 end of date range |
|
||||
| loggedInUsersTz | string | No | User's timezone |
|
||||
| credentialId | number | No | Specific calendar credential ID |
|
||||
|
||||
### Response
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"data": [
|
||||
{
|
||||
"start": "2024-01-15T10:00:00.000Z",
|
||||
"end": "2024-01-15T11:00:00.000Z",
|
||||
"title": "Existing Meeting",
|
||||
"source": "google_calendar"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Routing Form Slots
|
||||
|
||||
For routing forms that direct to different event types:
|
||||
|
||||
```http
|
||||
POST /v2/routing-forms/{routingFormId}/calculate-slots
|
||||
```
|
||||
|
||||
### Request Body
|
||||
|
||||
```json
|
||||
{
|
||||
"startTime": "2024-01-15T00:00:00Z",
|
||||
"endTime": "2024-01-22T00:00:00Z",
|
||||
"timeZone": "America/New_York",
|
||||
"responses": {
|
||||
"field1": "value1",
|
||||
"field2": "value2"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Understanding Slot Availability
|
||||
|
||||
Slots are calculated based on:
|
||||
|
||||
1. **User's Schedule**: Working hours defined in their schedule
|
||||
2. **Existing Bookings**: Times already booked
|
||||
3. **Calendar Busy Times**: Events from connected calendars
|
||||
4. **Buffer Times**: Before/after event buffers
|
||||
5. **Minimum Notice**: Minimum booking notice period
|
||||
6. **Booking Limits**: Daily/weekly/monthly booking limits
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Efficient Slot Fetching
|
||||
|
||||
1. **Limit date range**: Request only the dates you need to display
|
||||
2. **Cache results**: Slots don't change frequently, cache for short periods
|
||||
3. **Use timezone parameter**: Request slots in user's timezone to avoid conversion
|
||||
|
||||
### Preventing Double Bookings
|
||||
|
||||
1. **Reserve slots**: Use slot reservations for multi-step booking flows
|
||||
2. **Handle expiration**: Reservations expire - handle gracefully
|
||||
3. **Verify before booking**: Always verify slot is still available before creating booking
|
||||
|
||||
### Example Booking Flow
|
||||
|
||||
```
|
||||
1. User selects date range
|
||||
GET /v2/slots?startTime=...&endTime=...&eventTypeId=123
|
||||
|
||||
2. User selects a slot
|
||||
POST /v2/slots/reservations
|
||||
{
|
||||
"eventTypeId": 123,
|
||||
"slotUtcStartDate": "2024-01-15T09:00:00Z",
|
||||
"slotUtcEndDate": "2024-01-15T09:30:00Z"
|
||||
}
|
||||
|
||||
3. User fills booking form
|
||||
|
||||
4. Create booking
|
||||
POST /v2/bookings
|
||||
{
|
||||
"start": "2024-01-15T09:00:00Z",
|
||||
"eventTypeId": 123,
|
||||
"attendee": {...}
|
||||
}
|
||||
|
||||
5. If user abandons, release reservation
|
||||
DELETE /v2/slots/reservations/{uid}
|
||||
```
|
||||
|
||||
## Timezone Handling
|
||||
|
||||
All times in the API are in UTC (ISO 8601 format). Use the `timeZone` parameter to receive slots in a specific timezone for display purposes.
|
||||
|
||||
```http
|
||||
GET /v2/slots?startTime=2024-01-15T00:00:00Z&endTime=2024-01-22T00:00:00Z&eventTypeId=123&timeZone=Europe/London
|
||||
```
|
||||
|
||||
The response times will still be in UTC, but the slot calculation will respect the user's timezone for determining available hours.
|
||||
@@ -0,0 +1,330 @@
|
||||
# Webhooks API Reference
|
||||
|
||||
Detailed documentation for webhook management endpoints in the Cal.diy API v2.
|
||||
|
||||
## Endpoints Overview
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| GET | /v2/webhooks | List webhooks |
|
||||
| POST | /v2/webhooks | Create a webhook |
|
||||
| GET | /v2/webhooks/{webhookId} | Get a webhook |
|
||||
| PATCH | /v2/webhooks/{webhookId} | Update a webhook |
|
||||
| DELETE | /v2/webhooks/{webhookId} | Delete a webhook |
|
||||
|
||||
## List Webhooks
|
||||
|
||||
```http
|
||||
GET /v2/webhooks
|
||||
```
|
||||
|
||||
### Response
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"data": [
|
||||
{
|
||||
"id": "webhook-id-123",
|
||||
"subscriberUrl": "https://your-app.com/webhook",
|
||||
"triggers": ["BOOKING_CREATED", "BOOKING_CANCELLED"],
|
||||
"active": true,
|
||||
"payloadTemplate": null,
|
||||
"secret": "whsec_..."
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Create a Webhook
|
||||
|
||||
```http
|
||||
POST /v2/webhooks
|
||||
```
|
||||
|
||||
### Request Body
|
||||
|
||||
```json
|
||||
{
|
||||
"subscriberUrl": "https://your-app.com/webhook",
|
||||
"triggers": ["BOOKING_CREATED", "BOOKING_CANCELLED", "BOOKING_RESCHEDULED"],
|
||||
"active": true,
|
||||
"payloadTemplate": null,
|
||||
"secret": "your-webhook-secret"
|
||||
}
|
||||
```
|
||||
|
||||
### Fields
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| subscriberUrl | string | Yes | URL to receive webhook payloads |
|
||||
| triggers | array | Yes | Events that trigger the webhook |
|
||||
| active | boolean | No | Enable/disable webhook (default: true) |
|
||||
| payloadTemplate | string | No | Custom payload template |
|
||||
| secret | string | No | Secret for signature verification |
|
||||
|
||||
## Webhook Triggers
|
||||
|
||||
| Trigger | Description |
|
||||
|---------|-------------|
|
||||
| BOOKING_CREATED | New booking created |
|
||||
| BOOKING_CANCELLED | Booking cancelled |
|
||||
| BOOKING_RESCHEDULED | Booking rescheduled |
|
||||
| BOOKING_CONFIRMED | Pending booking confirmed |
|
||||
| BOOKING_REJECTED | Booking rejected |
|
||||
| BOOKING_REQUESTED | Booking request received (requires confirmation) |
|
||||
| BOOKING_PAYMENT_INITIATED | Payment started |
|
||||
| BOOKING_NO_SHOW_UPDATED | Attendee marked as no-show |
|
||||
| MEETING_STARTED | Video meeting started |
|
||||
| MEETING_ENDED | Video meeting ended |
|
||||
| RECORDING_READY | Meeting recording available |
|
||||
| INSTANT_MEETING | Instant meeting created |
|
||||
| RECORDING_TRANSCRIPTION_GENERATED | Transcription ready |
|
||||
| FORM_SUBMITTED | Routing form submitted |
|
||||
|
||||
## Webhook Payload
|
||||
|
||||
### BOOKING_CREATED Payload
|
||||
|
||||
```json
|
||||
{
|
||||
"triggerEvent": "BOOKING_CREATED",
|
||||
"createdAt": "2024-01-15T10:00:00.000Z",
|
||||
"payload": {
|
||||
"type": "30min",
|
||||
"title": "30 Minute Meeting",
|
||||
"description": "Meeting description",
|
||||
"startTime": "2024-01-15T10:00:00.000Z",
|
||||
"endTime": "2024-01-15T10:30:00.000Z",
|
||||
"organizer": {
|
||||
"id": 123,
|
||||
"name": "Jane Smith",
|
||||
"email": "jane@company.com",
|
||||
"timeZone": "America/New_York"
|
||||
},
|
||||
"attendees": [
|
||||
{
|
||||
"name": "John Doe",
|
||||
"email": "john@example.com",
|
||||
"timeZone": "America/New_York"
|
||||
}
|
||||
],
|
||||
"location": "https://cal.com/video/abc123",
|
||||
"destinationCalendar": {
|
||||
"integration": "google_calendar",
|
||||
"externalId": "calendar-id"
|
||||
},
|
||||
"uid": "booking-uid-123",
|
||||
"metadata": {},
|
||||
"responses": {
|
||||
"name": "John Doe",
|
||||
"email": "john@example.com"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### BOOKING_CANCELLED Payload
|
||||
|
||||
```json
|
||||
{
|
||||
"triggerEvent": "BOOKING_CANCELLED",
|
||||
"createdAt": "2024-01-15T12:00:00.000Z",
|
||||
"payload": {
|
||||
"uid": "booking-uid-123",
|
||||
"title": "30 Minute Meeting",
|
||||
"startTime": "2024-01-15T10:00:00.000Z",
|
||||
"endTime": "2024-01-15T10:30:00.000Z",
|
||||
"cancellationReason": "Schedule conflict",
|
||||
"organizer": {...},
|
||||
"attendees": [...]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### BOOKING_RESCHEDULED Payload
|
||||
|
||||
```json
|
||||
{
|
||||
"triggerEvent": "BOOKING_RESCHEDULED",
|
||||
"createdAt": "2024-01-15T11:00:00.000Z",
|
||||
"payload": {
|
||||
"uid": "new-booking-uid",
|
||||
"rescheduleUid": "original-booking-uid",
|
||||
"title": "30 Minute Meeting",
|
||||
"startTime": "2024-01-16T14:00:00.000Z",
|
||||
"endTime": "2024-01-16T14:30:00.000Z",
|
||||
"reschedulingReason": "Conflict with another meeting",
|
||||
"organizer": {...},
|
||||
"attendees": [...]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Signature Verification
|
||||
|
||||
Webhooks include a signature header for verification:
|
||||
|
||||
```
|
||||
X-Cal-Signature-256: sha256=<signature>
|
||||
```
|
||||
|
||||
### Verification Example (Node.js)
|
||||
|
||||
```javascript
|
||||
const crypto = require('crypto');
|
||||
|
||||
function verifyWebhookSignature(payload, signature, secret) {
|
||||
const expectedSignature = crypto
|
||||
.createHmac('sha256', secret)
|
||||
.update(payload)
|
||||
.digest('hex');
|
||||
|
||||
return `sha256=${expectedSignature}` === signature;
|
||||
}
|
||||
|
||||
// In your webhook handler
|
||||
app.post('/webhook', (req, res) => {
|
||||
const signature = req.headers['x-cal-signature-256'];
|
||||
const payload = JSON.stringify(req.body);
|
||||
|
||||
if (!verifyWebhookSignature(payload, signature, process.env.WEBHOOK_SECRET)) {
|
||||
return res.status(401).send('Invalid signature');
|
||||
}
|
||||
|
||||
// Process webhook
|
||||
const { triggerEvent, payload: data } = req.body;
|
||||
// ...
|
||||
|
||||
res.status(200).send('OK');
|
||||
});
|
||||
```
|
||||
|
||||
## Get a Webhook
|
||||
|
||||
```http
|
||||
GET /v2/webhooks/{webhookId}
|
||||
```
|
||||
|
||||
## Update a Webhook
|
||||
|
||||
```http
|
||||
PATCH /v2/webhooks/{webhookId}
|
||||
```
|
||||
|
||||
### Request Body
|
||||
|
||||
```json
|
||||
{
|
||||
"active": false,
|
||||
"triggers": ["BOOKING_CREATED"]
|
||||
}
|
||||
```
|
||||
|
||||
## Delete a Webhook
|
||||
|
||||
```http
|
||||
DELETE /v2/webhooks/{webhookId}
|
||||
```
|
||||
|
||||
## Event Type Webhooks
|
||||
|
||||
Create webhooks specific to an event type:
|
||||
|
||||
### List Event Type Webhooks
|
||||
|
||||
```http
|
||||
GET /v2/event-types/{eventTypeId}/webhooks
|
||||
```
|
||||
|
||||
### Create Event Type Webhook
|
||||
|
||||
```http
|
||||
POST /v2/event-types/{eventTypeId}/webhooks
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"subscriberUrl": "https://your-app.com/webhook",
|
||||
"triggers": ["BOOKING_CREATED"],
|
||||
"active": true
|
||||
}
|
||||
```
|
||||
|
||||
## Organization Webhooks
|
||||
|
||||
For organization-wide webhooks:
|
||||
|
||||
### List Organization Webhooks
|
||||
|
||||
```http
|
||||
GET /v2/organizations/{orgId}/webhooks
|
||||
```
|
||||
|
||||
### Create Organization Webhook
|
||||
|
||||
```http
|
||||
POST /v2/organizations/{orgId}/webhooks
|
||||
```
|
||||
|
||||
## OAuth Client Webhooks
|
||||
|
||||
For platform integrations:
|
||||
|
||||
### List OAuth Client Webhooks
|
||||
|
||||
```http
|
||||
GET /v2/oauth-clients/{clientId}/webhooks
|
||||
```
|
||||
|
||||
### Create OAuth Client Webhook
|
||||
|
||||
```http
|
||||
POST /v2/oauth-clients/{clientId}/webhooks
|
||||
```
|
||||
|
||||
## Custom Payload Templates
|
||||
|
||||
Customize webhook payloads using templates:
|
||||
|
||||
```json
|
||||
{
|
||||
"subscriberUrl": "https://your-app.com/webhook",
|
||||
"triggers": ["BOOKING_CREATED"],
|
||||
"payloadTemplate": "{\"event\": \"{{triggerEvent}}\", \"booking_id\": \"{{payload.uid}}\", \"attendee\": \"{{payload.attendees[0].email}}\"}"
|
||||
}
|
||||
```
|
||||
|
||||
### Available Template Variables
|
||||
|
||||
- `{{triggerEvent}}` - Event type
|
||||
- `{{payload.uid}}` - Booking UID
|
||||
- `{{payload.title}}` - Event title
|
||||
- `{{payload.startTime}}` - Start time
|
||||
- `{{payload.endTime}}` - End time
|
||||
- `{{payload.organizer.name}}` - Organizer name
|
||||
- `{{payload.organizer.email}}` - Organizer email
|
||||
- `{{payload.attendees[0].name}}` - First attendee name
|
||||
- `{{payload.attendees[0].email}}` - First attendee email
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Always verify signatures**: Use the webhook secret to verify payloads
|
||||
2. **Respond quickly**: Return 200 within 5 seconds, process async if needed
|
||||
3. **Handle retries**: Webhooks are retried on failure, implement idempotency
|
||||
4. **Use HTTPS**: Always use HTTPS endpoints for security
|
||||
5. **Log payloads**: Store webhook payloads for debugging
|
||||
6. **Monitor failures**: Track webhook delivery failures
|
||||
|
||||
## Retry Policy
|
||||
|
||||
Failed webhook deliveries are retried with exponential backoff:
|
||||
|
||||
- 1st retry: 1 minute
|
||||
- 2nd retry: 5 minutes
|
||||
- 3rd retry: 30 minutes
|
||||
- 4th retry: 2 hours
|
||||
- 5th retry: 24 hours
|
||||
|
||||
After 5 failed attempts, the webhook is marked as failed.
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,125 @@
|
||||
---
|
||||
name: vercel-react-best-practices
|
||||
description: React and Next.js performance optimization guidelines from Vercel Engineering. This skill should be used when writing, reviewing, or refactoring React/Next.js code to ensure optimal performance patterns. Triggers on tasks involving React components, Next.js pages, data fetching, bundle optimization, or performance improvements.
|
||||
license: MIT
|
||||
metadata:
|
||||
author: vercel
|
||||
version: "1.0.0"
|
||||
---
|
||||
|
||||
# Vercel React Best Practices
|
||||
|
||||
Comprehensive performance optimization guide for React and Next.js applications, maintained by Vercel. Contains 45 rules across 8 categories, prioritized by impact to guide automated refactoring and code generation.
|
||||
|
||||
## When to Apply
|
||||
|
||||
Reference these guidelines when:
|
||||
- Writing new React components or Next.js pages
|
||||
- Implementing data fetching (client or server-side)
|
||||
- Reviewing code for performance issues
|
||||
- Refactoring existing React/Next.js code
|
||||
- Optimizing bundle size or load times
|
||||
|
||||
## Rule Categories by Priority
|
||||
|
||||
| Priority | Category | Impact | Prefix |
|
||||
|----------|----------|--------|--------|
|
||||
| 1 | Eliminating Waterfalls | CRITICAL | `async-` |
|
||||
| 2 | Bundle Size Optimization | CRITICAL | `bundle-` |
|
||||
| 3 | Server-Side Performance | HIGH | `server-` |
|
||||
| 4 | Client-Side Data Fetching | MEDIUM-HIGH | `client-` |
|
||||
| 5 | Re-render Optimization | MEDIUM | `rerender-` |
|
||||
| 6 | Rendering Performance | MEDIUM | `rendering-` |
|
||||
| 7 | JavaScript Performance | LOW-MEDIUM | `js-` |
|
||||
| 8 | Advanced Patterns | LOW | `advanced-` |
|
||||
|
||||
## Quick Reference
|
||||
|
||||
### 1. Eliminating Waterfalls (CRITICAL)
|
||||
|
||||
- `async-defer-await` - Move await into branches where actually used
|
||||
- `async-parallel` - Use Promise.all() for independent operations
|
||||
- `async-dependencies` - Use better-all for partial dependencies
|
||||
- `async-api-routes` - Start promises early, await late in API routes
|
||||
- `async-suspense-boundaries` - Use Suspense to stream content
|
||||
|
||||
### 2. Bundle Size Optimization (CRITICAL)
|
||||
|
||||
- `bundle-barrel-imports` - Import directly, avoid barrel files
|
||||
- `bundle-dynamic-imports` - Use next/dynamic for heavy components
|
||||
- `bundle-defer-third-party` - Load analytics/logging after hydration
|
||||
- `bundle-conditional` - Load modules only when feature is activated
|
||||
- `bundle-preload` - Preload on hover/focus for perceived speed
|
||||
|
||||
### 3. Server-Side Performance (HIGH)
|
||||
|
||||
- `server-cache-react` - Use React.cache() for per-request deduplication
|
||||
- `server-cache-lru` - Use LRU cache for cross-request caching
|
||||
- `server-serialization` - Minimize data passed to client components
|
||||
- `server-parallel-fetching` - Restructure components to parallelize fetches
|
||||
- `server-after-nonblocking` - Use after() for non-blocking operations
|
||||
|
||||
### 4. Client-Side Data Fetching (MEDIUM-HIGH)
|
||||
|
||||
- `client-swr-dedup` - Use SWR for automatic request deduplication
|
||||
- `client-event-listeners` - Deduplicate global event listeners
|
||||
|
||||
### 5. Re-render Optimization (MEDIUM)
|
||||
|
||||
- `rerender-defer-reads` - Don't subscribe to state only used in callbacks
|
||||
- `rerender-memo` - Extract expensive work into memoized components
|
||||
- `rerender-dependencies` - Use primitive dependencies in effects
|
||||
- `rerender-derived-state` - Subscribe to derived booleans, not raw values
|
||||
- `rerender-functional-setstate` - Use functional setState for stable callbacks
|
||||
- `rerender-lazy-state-init` - Pass function to useState for expensive values
|
||||
- `rerender-transitions` - Use startTransition for non-urgent updates
|
||||
|
||||
### 6. Rendering Performance (MEDIUM)
|
||||
|
||||
- `rendering-animate-svg-wrapper` - Animate div wrapper, not SVG element
|
||||
- `rendering-content-visibility` - Use content-visibility for long lists
|
||||
- `rendering-hoist-jsx` - Extract static JSX outside components
|
||||
- `rendering-svg-precision` - Reduce SVG coordinate precision
|
||||
- `rendering-hydration-no-flicker` - Use inline script for client-only data
|
||||
- `rendering-activity` - Use Activity component for show/hide
|
||||
- `rendering-conditional-render` - Use ternary, not && for conditionals
|
||||
|
||||
### 7. JavaScript Performance (LOW-MEDIUM)
|
||||
|
||||
- `js-batch-dom-css` - Group CSS changes via classes or cssText
|
||||
- `js-index-maps` - Build Map for repeated lookups
|
||||
- `js-cache-property-access` - Cache object properties in loops
|
||||
- `js-cache-function-results` - Cache function results in module-level Map
|
||||
- `js-cache-storage` - Cache localStorage/sessionStorage reads
|
||||
- `js-combine-iterations` - Combine multiple filter/map into one loop
|
||||
- `js-length-check-first` - Check array length before expensive comparison
|
||||
- `js-early-exit` - Return early from functions
|
||||
- `js-hoist-regexp` - Hoist RegExp creation outside loops
|
||||
- `js-min-max-loop` - Use loop for min/max instead of sort
|
||||
- `js-set-map-lookups` - Use Set/Map for O(1) lookups
|
||||
- `js-tosorted-immutable` - Use toSorted() for immutability
|
||||
|
||||
### 8. Advanced Patterns (LOW)
|
||||
|
||||
- `advanced-event-handler-refs` - Store event handlers in refs
|
||||
- `advanced-use-latest` - useLatest for stable callback refs
|
||||
|
||||
## How to Use
|
||||
|
||||
Read individual rule files for detailed explanations and code examples:
|
||||
|
||||
```
|
||||
rules/async-parallel.md
|
||||
rules/bundle-barrel-imports.md
|
||||
rules/_sections.md
|
||||
```
|
||||
|
||||
Each rule file contains:
|
||||
- Brief explanation of why it matters
|
||||
- Incorrect code example with explanation
|
||||
- Correct code example with explanation
|
||||
- Additional context and references
|
||||
|
||||
## Full Compiled Document
|
||||
|
||||
For the complete guide with all rules expanded: `AGENTS.md`
|
||||
@@ -0,0 +1,55 @@
|
||||
---
|
||||
title: Store Event Handlers in Refs
|
||||
impact: LOW
|
||||
impactDescription: stable subscriptions
|
||||
tags: advanced, hooks, refs, event-handlers, optimization
|
||||
---
|
||||
|
||||
## Store Event Handlers in Refs
|
||||
|
||||
Store callbacks in refs when used in effects that shouldn't re-subscribe on callback changes.
|
||||
|
||||
**Incorrect (re-subscribes on every render):**
|
||||
|
||||
```tsx
|
||||
function useWindowEvent(event: string, handler: (e: Event) => void) {
|
||||
useEffect(() => {
|
||||
window.addEventListener(event, handler)
|
||||
return () => window.removeEventListener(event, handler)
|
||||
}, [event, handler])
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (stable subscription):**
|
||||
|
||||
```tsx
|
||||
function useWindowEvent(event: string, handler: (e: Event) => void) {
|
||||
const handlerRef = useRef(handler)
|
||||
useEffect(() => {
|
||||
handlerRef.current = handler
|
||||
}, [handler])
|
||||
|
||||
useEffect(() => {
|
||||
const listener = (e: Event) => handlerRef.current(e)
|
||||
window.addEventListener(event, listener)
|
||||
return () => window.removeEventListener(event, listener)
|
||||
}, [event])
|
||||
}
|
||||
```
|
||||
|
||||
**Alternative: use `useEffectEvent` if you're on latest React:**
|
||||
|
||||
```tsx
|
||||
import { useEffectEvent } from 'react'
|
||||
|
||||
function useWindowEvent(event: string, handler: () => void) {
|
||||
const onEvent = useEffectEvent(handler)
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener(event, onEvent)
|
||||
return () => window.removeEventListener(event, onEvent)
|
||||
}, [event])
|
||||
}
|
||||
```
|
||||
|
||||
`useEffectEvent` provides a cleaner API for the same pattern: it creates a stable function reference that always calls the latest version of the handler.
|
||||
@@ -0,0 +1,49 @@
|
||||
---
|
||||
title: useLatest for Stable Callback Refs
|
||||
impact: LOW
|
||||
impactDescription: prevents effect re-runs
|
||||
tags: advanced, hooks, useLatest, refs, optimization
|
||||
---
|
||||
|
||||
## useLatest for Stable Callback Refs
|
||||
|
||||
Access latest values in callbacks without adding them to dependency arrays. Prevents effect re-runs while avoiding stale closures.
|
||||
|
||||
**Implementation:**
|
||||
|
||||
```typescript
|
||||
function useLatest<T>(value: T) {
|
||||
const ref = useRef(value)
|
||||
useEffect(() => {
|
||||
ref.current = value
|
||||
}, [value])
|
||||
return ref
|
||||
}
|
||||
```
|
||||
|
||||
**Incorrect (effect re-runs on every callback change):**
|
||||
|
||||
```tsx
|
||||
function SearchInput({ onSearch }: { onSearch: (q: string) => void }) {
|
||||
const [query, setQuery] = useState('')
|
||||
|
||||
useEffect(() => {
|
||||
const timeout = setTimeout(() => onSearch(query), 300)
|
||||
return () => clearTimeout(timeout)
|
||||
}, [query, onSearch])
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (stable effect, fresh callback):**
|
||||
|
||||
```tsx
|
||||
function SearchInput({ onSearch }: { onSearch: (q: string) => void }) {
|
||||
const [query, setQuery] = useState('')
|
||||
const onSearchRef = useLatest(onSearch)
|
||||
|
||||
useEffect(() => {
|
||||
const timeout = setTimeout(() => onSearchRef.current(query), 300)
|
||||
return () => clearTimeout(timeout)
|
||||
}, [query])
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,38 @@
|
||||
---
|
||||
title: Prevent Waterfall Chains in API Routes
|
||||
impact: CRITICAL
|
||||
impactDescription: 2-10× improvement
|
||||
tags: api-routes, server-actions, waterfalls, parallelization
|
||||
---
|
||||
|
||||
## Prevent Waterfall Chains in API Routes
|
||||
|
||||
In API routes and Server Actions, start independent operations immediately, even if you don't await them yet.
|
||||
|
||||
**Incorrect (config waits for auth, data waits for both):**
|
||||
|
||||
```typescript
|
||||
export async function GET(request: Request) {
|
||||
const session = await auth()
|
||||
const config = await fetchConfig()
|
||||
const data = await fetchData(session.user.id)
|
||||
return Response.json({ data, config })
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (auth and config start immediately):**
|
||||
|
||||
```typescript
|
||||
export async function GET(request: Request) {
|
||||
const sessionPromise = auth()
|
||||
const configPromise = fetchConfig()
|
||||
const session = await sessionPromise
|
||||
const [config, data] = await Promise.all([
|
||||
configPromise,
|
||||
fetchData(session.user.id)
|
||||
])
|
||||
return Response.json({ data, config })
|
||||
}
|
||||
```
|
||||
|
||||
For operations with more complex dependency chains, use `better-all` to automatically maximize parallelism (see Dependency-Based Parallelization).
|
||||
@@ -0,0 +1,80 @@
|
||||
---
|
||||
title: Defer Await Until Needed
|
||||
impact: HIGH
|
||||
impactDescription: avoids blocking unused code paths
|
||||
tags: async, await, conditional, optimization
|
||||
---
|
||||
|
||||
## Defer Await Until Needed
|
||||
|
||||
Move `await` operations into the branches where they're actually used to avoid blocking code paths that don't need them.
|
||||
|
||||
**Incorrect (blocks both branches):**
|
||||
|
||||
```typescript
|
||||
async function handleRequest(userId: string, skipProcessing: boolean) {
|
||||
const userData = await fetchUserData(userId)
|
||||
|
||||
if (skipProcessing) {
|
||||
// Returns immediately but still waited for userData
|
||||
return { skipped: true }
|
||||
}
|
||||
|
||||
// Only this branch uses userData
|
||||
return processUserData(userData)
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (only blocks when needed):**
|
||||
|
||||
```typescript
|
||||
async function handleRequest(userId: string, skipProcessing: boolean) {
|
||||
if (skipProcessing) {
|
||||
// Returns immediately without waiting
|
||||
return { skipped: true }
|
||||
}
|
||||
|
||||
// Fetch only when needed
|
||||
const userData = await fetchUserData(userId)
|
||||
return processUserData(userData)
|
||||
}
|
||||
```
|
||||
|
||||
**Another example (early return optimization):**
|
||||
|
||||
```typescript
|
||||
// Incorrect: always fetches permissions
|
||||
async function updateResource(resourceId: string, userId: string) {
|
||||
const permissions = await fetchPermissions(userId)
|
||||
const resource = await getResource(resourceId)
|
||||
|
||||
if (!resource) {
|
||||
return { error: 'Not found' }
|
||||
}
|
||||
|
||||
if (!permissions.canEdit) {
|
||||
return { error: 'Forbidden' }
|
||||
}
|
||||
|
||||
return await updateResourceData(resource, permissions)
|
||||
}
|
||||
|
||||
// Correct: fetches only when needed
|
||||
async function updateResource(resourceId: string, userId: string) {
|
||||
const resource = await getResource(resourceId)
|
||||
|
||||
if (!resource) {
|
||||
return { error: 'Not found' }
|
||||
}
|
||||
|
||||
const permissions = await fetchPermissions(userId)
|
||||
|
||||
if (!permissions.canEdit) {
|
||||
return { error: 'Forbidden' }
|
||||
}
|
||||
|
||||
return await updateResourceData(resource, permissions)
|
||||
}
|
||||
```
|
||||
|
||||
This optimization is especially valuable when the skipped branch is frequently taken, or when the deferred operation is expensive.
|
||||
@@ -0,0 +1,36 @@
|
||||
---
|
||||
title: Dependency-Based Parallelization
|
||||
impact: CRITICAL
|
||||
impactDescription: 2-10× improvement
|
||||
tags: async, parallelization, dependencies, better-all
|
||||
---
|
||||
|
||||
## Dependency-Based Parallelization
|
||||
|
||||
For operations with partial dependencies, use `better-all` to maximize parallelism. It automatically starts each task at the earliest possible moment.
|
||||
|
||||
**Incorrect (profile waits for config unnecessarily):**
|
||||
|
||||
```typescript
|
||||
const [user, config] = await Promise.all([
|
||||
fetchUser(),
|
||||
fetchConfig()
|
||||
])
|
||||
const profile = await fetchProfile(user.id)
|
||||
```
|
||||
|
||||
**Correct (config and profile run in parallel):**
|
||||
|
||||
```typescript
|
||||
import { all } from 'better-all'
|
||||
|
||||
const { user, config, profile } = await all({
|
||||
async user() { return fetchUser() },
|
||||
async config() { return fetchConfig() },
|
||||
async profile() {
|
||||
return fetchProfile((await this.$.user).id)
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
Reference: [https://github.com/shuding/better-all](https://github.com/shuding/better-all)
|
||||
@@ -0,0 +1,28 @@
|
||||
---
|
||||
title: Promise.all() for Independent Operations
|
||||
impact: CRITICAL
|
||||
impactDescription: 2-10× improvement
|
||||
tags: async, parallelization, promises, waterfalls
|
||||
---
|
||||
|
||||
## Promise.all() for Independent Operations
|
||||
|
||||
When async operations have no interdependencies, execute them concurrently using `Promise.all()`.
|
||||
|
||||
**Incorrect (sequential execution, 3 round trips):**
|
||||
|
||||
```typescript
|
||||
const user = await fetchUser()
|
||||
const posts = await fetchPosts()
|
||||
const comments = await fetchComments()
|
||||
```
|
||||
|
||||
**Correct (parallel execution, 1 round trip):**
|
||||
|
||||
```typescript
|
||||
const [user, posts, comments] = await Promise.all([
|
||||
fetchUser(),
|
||||
fetchPosts(),
|
||||
fetchComments()
|
||||
])
|
||||
```
|
||||
@@ -0,0 +1,99 @@
|
||||
---
|
||||
title: Strategic Suspense Boundaries
|
||||
impact: HIGH
|
||||
impactDescription: faster initial paint
|
||||
tags: async, suspense, streaming, layout-shift
|
||||
---
|
||||
|
||||
## Strategic Suspense Boundaries
|
||||
|
||||
Instead of awaiting data in async components before returning JSX, use Suspense boundaries to show the wrapper UI faster while data loads.
|
||||
|
||||
**Incorrect (wrapper blocked by data fetching):**
|
||||
|
||||
```tsx
|
||||
async function Page() {
|
||||
const data = await fetchData() // Blocks entire page
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div>Sidebar</div>
|
||||
<div>Header</div>
|
||||
<div>
|
||||
<DataDisplay data={data} />
|
||||
</div>
|
||||
<div>Footer</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
The entire layout waits for data even though only the middle section needs it.
|
||||
|
||||
**Correct (wrapper shows immediately, data streams in):**
|
||||
|
||||
```tsx
|
||||
function Page() {
|
||||
return (
|
||||
<div>
|
||||
<div>Sidebar</div>
|
||||
<div>Header</div>
|
||||
<div>
|
||||
<Suspense fallback={<Skeleton />}>
|
||||
<DataDisplay />
|
||||
</Suspense>
|
||||
</div>
|
||||
<div>Footer</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
async function DataDisplay() {
|
||||
const data = await fetchData() // Only blocks this component
|
||||
return <div>{data.content}</div>
|
||||
}
|
||||
```
|
||||
|
||||
Sidebar, Header, and Footer render immediately. Only DataDisplay waits for data.
|
||||
|
||||
**Alternative (share promise across components):**
|
||||
|
||||
```tsx
|
||||
function Page() {
|
||||
// Start fetch immediately, but don't await
|
||||
const dataPromise = fetchData()
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div>Sidebar</div>
|
||||
<div>Header</div>
|
||||
<Suspense fallback={<Skeleton />}>
|
||||
<DataDisplay dataPromise={dataPromise} />
|
||||
<DataSummary dataPromise={dataPromise} />
|
||||
</Suspense>
|
||||
<div>Footer</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function DataDisplay({ dataPromise }: { dataPromise: Promise<Data> }) {
|
||||
const data = use(dataPromise) // Unwraps the promise
|
||||
return <div>{data.content}</div>
|
||||
}
|
||||
|
||||
function DataSummary({ dataPromise }: { dataPromise: Promise<Data> }) {
|
||||
const data = use(dataPromise) // Reuses the same promise
|
||||
return <div>{data.summary}</div>
|
||||
}
|
||||
```
|
||||
|
||||
Both components share the same promise, so only one fetch occurs. Layout renders immediately while both components wait together.
|
||||
|
||||
**When NOT to use this pattern:**
|
||||
|
||||
- Critical data needed for layout decisions (affects positioning)
|
||||
- SEO-critical content above the fold
|
||||
- Small, fast queries where suspense overhead isn't worth it
|
||||
- When you want to avoid layout shift (loading → content jump)
|
||||
|
||||
**Trade-off:** Faster initial paint vs potential layout shift. Choose based on your UX priorities.
|
||||
@@ -0,0 +1,59 @@
|
||||
---
|
||||
title: Avoid Barrel File Imports
|
||||
impact: CRITICAL
|
||||
impactDescription: 200-800ms import cost, slow builds
|
||||
tags: bundle, imports, tree-shaking, barrel-files, performance
|
||||
---
|
||||
|
||||
## Avoid Barrel File Imports
|
||||
|
||||
Import directly from source files instead of barrel files to avoid loading thousands of unused modules. **Barrel files** are entry points that re-export multiple modules (e.g., `index.js` that does `export * from './module'`).
|
||||
|
||||
Popular icon and component libraries can have **up to 10,000 re-exports** in their entry file. For many React packages, **it takes 200-800ms just to import them**, affecting both development speed and production cold starts.
|
||||
|
||||
**Why tree-shaking doesn't help:** When a library is marked as external (not bundled), the bundler can't optimize it. If you bundle it to enable tree-shaking, builds become substantially slower analyzing the entire module graph.
|
||||
|
||||
**Incorrect (imports entire library):**
|
||||
|
||||
```tsx
|
||||
import { Check, X, Menu } from 'lucide-react'
|
||||
// Loads 1,583 modules, takes ~2.8s extra in dev
|
||||
// Runtime cost: 200-800ms on every cold start
|
||||
|
||||
import { Button, TextField } from '@mui/material'
|
||||
// Loads 2,225 modules, takes ~4.2s extra in dev
|
||||
```
|
||||
|
||||
**Correct (imports only what you need):**
|
||||
|
||||
```tsx
|
||||
import Check from 'lucide-react/dist/esm/icons/check'
|
||||
import X from 'lucide-react/dist/esm/icons/x'
|
||||
import Menu from 'lucide-react/dist/esm/icons/menu'
|
||||
// Loads only 3 modules (~2KB vs ~1MB)
|
||||
|
||||
import Button from '@mui/material/Button'
|
||||
import TextField from '@mui/material/TextField'
|
||||
// Loads only what you use
|
||||
```
|
||||
|
||||
**Alternative (Next.js 13.5+):**
|
||||
|
||||
```js
|
||||
// next.config.js - use optimizePackageImports
|
||||
module.exports = {
|
||||
experimental: {
|
||||
optimizePackageImports: ['lucide-react', '@mui/material']
|
||||
}
|
||||
}
|
||||
|
||||
// Then you can keep the ergonomic barrel imports:
|
||||
import { Check, X, Menu } from 'lucide-react'
|
||||
// Automatically transformed to direct imports at build time
|
||||
```
|
||||
|
||||
Direct imports provide 15-70% faster dev boot, 28% faster builds, 40% faster cold starts, and significantly faster HMR.
|
||||
|
||||
Libraries commonly affected: `lucide-react`, `@mui/material`, `@mui/icons-material`, `@tabler/icons-react`, `react-icons`, `@headlessui/react`, `@radix-ui/react-*`, `lodash`, `ramda`, `date-fns`, `rxjs`, `react-use`.
|
||||
|
||||
Reference: [How we optimized package imports in Next.js](https://vercel.com/blog/how-we-optimized-package-imports-in-next-js)
|
||||
@@ -0,0 +1,33 @@
|
||||
---
|
||||
title: Conditional Module Loading
|
||||
impact: HIGH
|
||||
impactDescription: loads large data only when needed
|
||||
tags: bundle, conditional-loading, lazy-loading
|
||||
---
|
||||
|
||||
## Conditional Module Loading
|
||||
|
||||
Load large data or modules only when a feature is activated.
|
||||
|
||||
**Example (lazy-load animation frames):**
|
||||
|
||||
```tsx
|
||||
function AnimationPlayer({ enabled }: { enabled: boolean }) {
|
||||
const [frames, setFrames] = useState<Frame[] | null>(null)
|
||||
const [loadError, setLoadError] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (enabled && !frames && !loadError && typeof window !== 'undefined') {
|
||||
import('./animation-frames.js')
|
||||
.then(mod => setFrames(mod.frames))
|
||||
.catch(() => setLoadError(true))
|
||||
}
|
||||
}, [enabled, frames, loadError])
|
||||
|
||||
if (loadError) return <ErrorMessage />
|
||||
if (!frames) return <Skeleton />
|
||||
return <Canvas frames={frames} />
|
||||
}
|
||||
```
|
||||
|
||||
The `typeof window !== 'undefined'` check prevents bundling this module for SSR, optimizing server bundle size and build speed.
|
||||
@@ -0,0 +1,49 @@
|
||||
---
|
||||
title: Defer Non-Critical Third-Party Libraries
|
||||
impact: MEDIUM
|
||||
impactDescription: loads after hydration
|
||||
tags: bundle, third-party, analytics, defer
|
||||
---
|
||||
|
||||
## Defer Non-Critical Third-Party Libraries
|
||||
|
||||
Analytics, logging, and error tracking don't block user interaction. Load them after hydration.
|
||||
|
||||
**Incorrect (blocks initial bundle):**
|
||||
|
||||
```tsx
|
||||
import { Analytics } from '@vercel/analytics/react'
|
||||
|
||||
export default function RootLayout({ children }) {
|
||||
return (
|
||||
<html>
|
||||
<body>
|
||||
{children}
|
||||
<Analytics />
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (loads after hydration):**
|
||||
|
||||
```tsx
|
||||
import dynamic from 'next/dynamic'
|
||||
|
||||
const Analytics = dynamic(
|
||||
() => import('@vercel/analytics/react').then(m => m.Analytics),
|
||||
{ ssr: false }
|
||||
)
|
||||
|
||||
export default function RootLayout({ children }) {
|
||||
return (
|
||||
<html>
|
||||
<body>
|
||||
{children}
|
||||
<Analytics />
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,35 @@
|
||||
---
|
||||
title: Dynamic Imports for Heavy Components
|
||||
impact: CRITICAL
|
||||
impactDescription: directly affects TTI and LCP
|
||||
tags: bundle, dynamic-import, code-splitting, next-dynamic
|
||||
---
|
||||
|
||||
## Dynamic Imports for Heavy Components
|
||||
|
||||
Use `next/dynamic` to lazy-load large components not needed on initial render.
|
||||
|
||||
**Incorrect (Monaco bundles with main chunk ~300KB):**
|
||||
|
||||
```tsx
|
||||
import { MonacoEditor } from './monaco-editor'
|
||||
|
||||
function CodePanel({ code }: { code: string }) {
|
||||
return <MonacoEditor value={code} />
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (Monaco loads on demand):**
|
||||
|
||||
```tsx
|
||||
import dynamic from 'next/dynamic'
|
||||
|
||||
const MonacoEditor = dynamic(
|
||||
() => import('./monaco-editor').then(m => m.MonacoEditor),
|
||||
{ ssr: false }
|
||||
)
|
||||
|
||||
function CodePanel({ code }: { code: string }) {
|
||||
return <MonacoEditor value={code} />
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,50 @@
|
||||
---
|
||||
title: Preload Based on User Intent
|
||||
impact: MEDIUM
|
||||
impactDescription: reduces perceived latency
|
||||
tags: bundle, preload, user-intent, hover
|
||||
---
|
||||
|
||||
## Preload Based on User Intent
|
||||
|
||||
Preload heavy bundles before they're needed to reduce perceived latency.
|
||||
|
||||
**Example (preload on hover/focus):**
|
||||
|
||||
```tsx
|
||||
function EditorButton({ onClick }: { onClick: () => void }) {
|
||||
const preload = () => {
|
||||
if (typeof window !== 'undefined') {
|
||||
void import('./monaco-editor')
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
onMouseEnter={preload}
|
||||
onFocus={preload}
|
||||
onClick={onClick}
|
||||
>
|
||||
Open Editor
|
||||
</button>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
**Example (preload when feature flag is enabled):**
|
||||
|
||||
```tsx
|
||||
function FlagsProvider({ children, flags }: Props) {
|
||||
useEffect(() => {
|
||||
if (flags.editorEnabled && typeof window !== 'undefined') {
|
||||
void import('./monaco-editor').then(mod => mod.init())
|
||||
}
|
||||
}, [flags.editorEnabled])
|
||||
|
||||
return <FlagsContext.Provider value={flags}>
|
||||
{children}
|
||||
</FlagsContext.Provider>
|
||||
}
|
||||
```
|
||||
|
||||
The `typeof window !== 'undefined'` check prevents bundling preloaded modules for SSR, optimizing server bundle size and build speed.
|
||||
@@ -0,0 +1,74 @@
|
||||
---
|
||||
title: Deduplicate Global Event Listeners
|
||||
impact: LOW
|
||||
impactDescription: single listener for N components
|
||||
tags: client, swr, event-listeners, subscription
|
||||
---
|
||||
|
||||
## Deduplicate Global Event Listeners
|
||||
|
||||
Use `useSWRSubscription()` to share global event listeners across component instances.
|
||||
|
||||
**Incorrect (N instances = N listeners):**
|
||||
|
||||
```tsx
|
||||
function useKeyboardShortcut(key: string, callback: () => void) {
|
||||
useEffect(() => {
|
||||
const handler = (e: KeyboardEvent) => {
|
||||
if (e.metaKey && e.key === key) {
|
||||
callback()
|
||||
}
|
||||
}
|
||||
window.addEventListener('keydown', handler)
|
||||
return () => window.removeEventListener('keydown', handler)
|
||||
}, [key, callback])
|
||||
}
|
||||
```
|
||||
|
||||
When using the `useKeyboardShortcut` hook multiple times, each instance will register a new listener.
|
||||
|
||||
**Correct (N instances = 1 listener):**
|
||||
|
||||
```tsx
|
||||
import useSWRSubscription from 'swr/subscription'
|
||||
|
||||
// Module-level Map to track callbacks per key
|
||||
const keyCallbacks = new Map<string, Set<() => void>>()
|
||||
|
||||
function useKeyboardShortcut(key: string, callback: () => void) {
|
||||
// Register this callback in the Map
|
||||
useEffect(() => {
|
||||
if (!keyCallbacks.has(key)) {
|
||||
keyCallbacks.set(key, new Set())
|
||||
}
|
||||
keyCallbacks.get(key)!.add(callback)
|
||||
|
||||
return () => {
|
||||
const set = keyCallbacks.get(key)
|
||||
if (set) {
|
||||
set.delete(callback)
|
||||
if (set.size === 0) {
|
||||
keyCallbacks.delete(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [key, callback])
|
||||
|
||||
useSWRSubscription('global-keydown', () => {
|
||||
const handler = (e: KeyboardEvent) => {
|
||||
if (e.metaKey && keyCallbacks.has(e.key)) {
|
||||
keyCallbacks.get(e.key)!.forEach(cb => cb())
|
||||
}
|
||||
}
|
||||
window.addEventListener('keydown', handler)
|
||||
return () => window.removeEventListener('keydown', handler)
|
||||
})
|
||||
}
|
||||
|
||||
function Profile() {
|
||||
// Multiple shortcuts will share the same listener
|
||||
useKeyboardShortcut('p', () => { /* ... */ })
|
||||
useKeyboardShortcut('k', () => { /* ... */ })
|
||||
// ...
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,56 @@
|
||||
---
|
||||
title: Use SWR for Automatic Deduplication
|
||||
impact: MEDIUM-HIGH
|
||||
impactDescription: automatic deduplication
|
||||
tags: client, swr, deduplication, data-fetching
|
||||
---
|
||||
|
||||
## Use SWR for Automatic Deduplication
|
||||
|
||||
SWR enables request deduplication, caching, and revalidation across component instances.
|
||||
|
||||
**Incorrect (no deduplication, each instance fetches):**
|
||||
|
||||
```tsx
|
||||
function UserList() {
|
||||
const [users, setUsers] = useState([])
|
||||
useEffect(() => {
|
||||
fetch('/api/users')
|
||||
.then(r => r.json())
|
||||
.then(setUsers)
|
||||
}, [])
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (multiple instances share one request):**
|
||||
|
||||
```tsx
|
||||
import useSWR from 'swr'
|
||||
|
||||
function UserList() {
|
||||
const { data: users } = useSWR('/api/users', fetcher)
|
||||
}
|
||||
```
|
||||
|
||||
**For immutable data:**
|
||||
|
||||
```tsx
|
||||
import { useImmutableSWR } from '@/lib/swr'
|
||||
|
||||
function StaticContent() {
|
||||
const { data } = useImmutableSWR('/api/config', fetcher)
|
||||
}
|
||||
```
|
||||
|
||||
**For mutations:**
|
||||
|
||||
```tsx
|
||||
import { useSWRMutation } from 'swr/mutation'
|
||||
|
||||
function UpdateButton() {
|
||||
const { trigger } = useSWRMutation('/api/user', updateUser)
|
||||
return <button onClick={() => trigger()}>Update</button>
|
||||
}
|
||||
```
|
||||
|
||||
Reference: [https://swr.vercel.app](https://swr.vercel.app)
|
||||
@@ -0,0 +1,82 @@
|
||||
---
|
||||
title: Batch DOM CSS Changes
|
||||
impact: MEDIUM
|
||||
impactDescription: reduces reflows/repaints
|
||||
tags: javascript, dom, css, performance, reflow
|
||||
---
|
||||
|
||||
## Batch DOM CSS Changes
|
||||
|
||||
Avoid changing styles one property at a time. Group multiple CSS changes together via classes or `cssText` to minimize browser reflows.
|
||||
|
||||
**Incorrect (multiple reflows):**
|
||||
|
||||
```typescript
|
||||
function updateElementStyles(element: HTMLElement) {
|
||||
// Each line triggers a reflow
|
||||
element.style.width = '100px'
|
||||
element.style.height = '200px'
|
||||
element.style.backgroundColor = 'blue'
|
||||
element.style.border = '1px solid black'
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (add class - single reflow):**
|
||||
|
||||
```typescript
|
||||
// CSS file
|
||||
.highlighted-box {
|
||||
width: 100px;
|
||||
height: 200px;
|
||||
background-color: blue;
|
||||
border: 1px solid black;
|
||||
}
|
||||
|
||||
// JavaScript
|
||||
function updateElementStyles(element: HTMLElement) {
|
||||
element.classList.add('highlighted-box')
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (change cssText - single reflow):**
|
||||
|
||||
```typescript
|
||||
function updateElementStyles(element: HTMLElement) {
|
||||
element.style.cssText = `
|
||||
width: 100px;
|
||||
height: 200px;
|
||||
background-color: blue;
|
||||
border: 1px solid black;
|
||||
`
|
||||
}
|
||||
```
|
||||
|
||||
**React example:**
|
||||
|
||||
```tsx
|
||||
// Incorrect: changing styles one by one
|
||||
function Box({ isHighlighted }: { isHighlighted: boolean }) {
|
||||
const ref = useRef<HTMLDivElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (ref.current && isHighlighted) {
|
||||
ref.current.style.width = '100px'
|
||||
ref.current.style.height = '200px'
|
||||
ref.current.style.backgroundColor = 'blue'
|
||||
}
|
||||
}, [isHighlighted])
|
||||
|
||||
return <div ref={ref}>Content</div>
|
||||
}
|
||||
|
||||
// Correct: toggle class
|
||||
function Box({ isHighlighted }: { isHighlighted: boolean }) {
|
||||
return (
|
||||
<div className={isHighlighted ? 'highlighted-box' : ''}>
|
||||
Content
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
Prefer CSS classes over inline styles when possible. Classes are cached by the browser and provide better separation of concerns.
|
||||
@@ -0,0 +1,80 @@
|
||||
---
|
||||
title: Cache Repeated Function Calls
|
||||
impact: MEDIUM
|
||||
impactDescription: avoid redundant computation
|
||||
tags: javascript, cache, memoization, performance
|
||||
---
|
||||
|
||||
## Cache Repeated Function Calls
|
||||
|
||||
Use a module-level Map to cache function results when the same function is called repeatedly with the same inputs during render.
|
||||
|
||||
**Incorrect (redundant computation):**
|
||||
|
||||
```typescript
|
||||
function ProjectList({ projects }: { projects: Project[] }) {
|
||||
return (
|
||||
<div>
|
||||
{projects.map(project => {
|
||||
// slugify() called 100+ times for same project names
|
||||
const slug = slugify(project.name)
|
||||
|
||||
return <ProjectCard key={project.id} slug={slug} />
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (cached results):**
|
||||
|
||||
```typescript
|
||||
// Module-level cache
|
||||
const slugifyCache = new Map<string, string>()
|
||||
|
||||
function cachedSlugify(text: string): string {
|
||||
if (slugifyCache.has(text)) {
|
||||
return slugifyCache.get(text)!
|
||||
}
|
||||
const result = slugify(text)
|
||||
slugifyCache.set(text, result)
|
||||
return result
|
||||
}
|
||||
|
||||
function ProjectList({ projects }: { projects: Project[] }) {
|
||||
return (
|
||||
<div>
|
||||
{projects.map(project => {
|
||||
// Computed only once per unique project name
|
||||
const slug = cachedSlugify(project.name)
|
||||
|
||||
return <ProjectCard key={project.id} slug={slug} />
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
**Simpler pattern for single-value functions:**
|
||||
|
||||
```typescript
|
||||
let isLoggedInCache: boolean | null = null
|
||||
|
||||
function isLoggedIn(): boolean {
|
||||
if (isLoggedInCache !== null) {
|
||||
return isLoggedInCache
|
||||
}
|
||||
|
||||
isLoggedInCache = document.cookie.includes('auth=')
|
||||
return isLoggedInCache
|
||||
}
|
||||
|
||||
// Clear cache when auth changes
|
||||
function onAuthChange() {
|
||||
isLoggedInCache = null
|
||||
}
|
||||
```
|
||||
|
||||
Use a Map (not a hook) so it works everywhere: utilities, event handlers, not just React components.
|
||||
|
||||
Reference: [How we made the Vercel Dashboard twice as fast](https://vercel.com/blog/how-we-made-the-vercel-dashboard-twice-as-fast)
|
||||
@@ -0,0 +1,28 @@
|
||||
---
|
||||
title: Cache Property Access in Loops
|
||||
impact: LOW-MEDIUM
|
||||
impactDescription: reduces lookups
|
||||
tags: javascript, loops, optimization, caching
|
||||
---
|
||||
|
||||
## Cache Property Access in Loops
|
||||
|
||||
Cache object property lookups in hot paths.
|
||||
|
||||
**Incorrect (3 lookups × N iterations):**
|
||||
|
||||
```typescript
|
||||
for (let i = 0; i < arr.length; i++) {
|
||||
process(obj.config.settings.value)
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (1 lookup total):**
|
||||
|
||||
```typescript
|
||||
const value = obj.config.settings.value
|
||||
const len = arr.length
|
||||
for (let i = 0; i < len; i++) {
|
||||
process(value)
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,70 @@
|
||||
---
|
||||
title: Cache Storage API Calls
|
||||
impact: LOW-MEDIUM
|
||||
impactDescription: reduces expensive I/O
|
||||
tags: javascript, localStorage, storage, caching, performance
|
||||
---
|
||||
|
||||
## Cache Storage API Calls
|
||||
|
||||
`localStorage`, `sessionStorage`, and `document.cookie` are synchronous and expensive. Cache reads in memory.
|
||||
|
||||
**Incorrect (reads storage on every call):**
|
||||
|
||||
```typescript
|
||||
function getTheme() {
|
||||
return localStorage.getItem('theme') ?? 'light'
|
||||
}
|
||||
// Called 10 times = 10 storage reads
|
||||
```
|
||||
|
||||
**Correct (Map cache):**
|
||||
|
||||
```typescript
|
||||
const storageCache = new Map<string, string | null>()
|
||||
|
||||
function getLocalStorage(key: string) {
|
||||
if (!storageCache.has(key)) {
|
||||
storageCache.set(key, localStorage.getItem(key))
|
||||
}
|
||||
return storageCache.get(key)
|
||||
}
|
||||
|
||||
function setLocalStorage(key: string, value: string) {
|
||||
localStorage.setItem(key, value)
|
||||
storageCache.set(key, value) // keep cache in sync
|
||||
}
|
||||
```
|
||||
|
||||
Use a Map (not a hook) so it works everywhere: utilities, event handlers, not just React components.
|
||||
|
||||
**Cookie caching:**
|
||||
|
||||
```typescript
|
||||
let cookieCache: Record<string, string> | null = null
|
||||
|
||||
function getCookie(name: string) {
|
||||
if (!cookieCache) {
|
||||
cookieCache = Object.fromEntries(
|
||||
document.cookie.split('; ').map(c => c.split('='))
|
||||
)
|
||||
}
|
||||
return cookieCache[name]
|
||||
}
|
||||
```
|
||||
|
||||
**Important (invalidate on external changes):**
|
||||
|
||||
If storage can change externally (another tab, server-set cookies), invalidate cache:
|
||||
|
||||
```typescript
|
||||
window.addEventListener('storage', (e) => {
|
||||
if (e.key) storageCache.delete(e.key)
|
||||
})
|
||||
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
if (document.visibilityState === 'visible') {
|
||||
storageCache.clear()
|
||||
}
|
||||
})
|
||||
```
|
||||
@@ -0,0 +1,32 @@
|
||||
---
|
||||
title: Combine Multiple Array Iterations
|
||||
impact: LOW-MEDIUM
|
||||
impactDescription: reduces iterations
|
||||
tags: javascript, arrays, loops, performance
|
||||
---
|
||||
|
||||
## Combine Multiple Array Iterations
|
||||
|
||||
Multiple `.filter()` or `.map()` calls iterate the array multiple times. Combine into one loop.
|
||||
|
||||
**Incorrect (3 iterations):**
|
||||
|
||||
```typescript
|
||||
const admins = users.filter(u => u.isAdmin)
|
||||
const testers = users.filter(u => u.isTester)
|
||||
const inactive = users.filter(u => !u.isActive)
|
||||
```
|
||||
|
||||
**Correct (1 iteration):**
|
||||
|
||||
```typescript
|
||||
const admins: User[] = []
|
||||
const testers: User[] = []
|
||||
const inactive: User[] = []
|
||||
|
||||
for (const user of users) {
|
||||
if (user.isAdmin) admins.push(user)
|
||||
if (user.isTester) testers.push(user)
|
||||
if (!user.isActive) inactive.push(user)
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,50 @@
|
||||
---
|
||||
title: Early Return from Functions
|
||||
impact: LOW-MEDIUM
|
||||
impactDescription: avoids unnecessary computation
|
||||
tags: javascript, functions, optimization, early-return
|
||||
---
|
||||
|
||||
## Early Return from Functions
|
||||
|
||||
Return early when result is determined to skip unnecessary processing.
|
||||
|
||||
**Incorrect (processes all items even after finding answer):**
|
||||
|
||||
```typescript
|
||||
function validateUsers(users: User[]) {
|
||||
let hasError = false
|
||||
let errorMessage = ''
|
||||
|
||||
for (const user of users) {
|
||||
if (!user.email) {
|
||||
hasError = true
|
||||
errorMessage = 'Email required'
|
||||
}
|
||||
if (!user.name) {
|
||||
hasError = true
|
||||
errorMessage = 'Name required'
|
||||
}
|
||||
// Continues checking all users even after error found
|
||||
}
|
||||
|
||||
return hasError ? { valid: false, error: errorMessage } : { valid: true }
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (returns immediately on first error):**
|
||||
|
||||
```typescript
|
||||
function validateUsers(users: User[]) {
|
||||
for (const user of users) {
|
||||
if (!user.email) {
|
||||
return { valid: false, error: 'Email required' }
|
||||
}
|
||||
if (!user.name) {
|
||||
return { valid: false, error: 'Name required' }
|
||||
}
|
||||
}
|
||||
|
||||
return { valid: true }
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,45 @@
|
||||
---
|
||||
title: Hoist RegExp Creation
|
||||
impact: LOW-MEDIUM
|
||||
impactDescription: avoids recreation
|
||||
tags: javascript, regexp, optimization, memoization
|
||||
---
|
||||
|
||||
## Hoist RegExp Creation
|
||||
|
||||
Don't create RegExp inside render. Hoist to module scope or memoize with `useMemo()`.
|
||||
|
||||
**Incorrect (new RegExp every render):**
|
||||
|
||||
```tsx
|
||||
function Highlighter({ text, query }: Props) {
|
||||
const regex = new RegExp(`(${query})`, 'gi')
|
||||
const parts = text.split(regex)
|
||||
return <>{parts.map((part, i) => ...)}</>
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (memoize or hoist):**
|
||||
|
||||
```tsx
|
||||
const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
||||
|
||||
function Highlighter({ text, query }: Props) {
|
||||
const regex = useMemo(
|
||||
() => new RegExp(`(${escapeRegex(query)})`, 'gi'),
|
||||
[query]
|
||||
)
|
||||
const parts = text.split(regex)
|
||||
return <>{parts.map((part, i) => ...)}</>
|
||||
}
|
||||
```
|
||||
|
||||
**Warning (global regex has mutable state):**
|
||||
|
||||
Global regex (`/g`) has mutable `lastIndex` state:
|
||||
|
||||
```typescript
|
||||
const regex = /foo/g
|
||||
regex.test('foo') // true, lastIndex = 3
|
||||
regex.test('foo') // false, lastIndex = 0
|
||||
```
|
||||
@@ -0,0 +1,37 @@
|
||||
---
|
||||
title: Build Index Maps for Repeated Lookups
|
||||
impact: LOW-MEDIUM
|
||||
impactDescription: 1M ops to 2K ops
|
||||
tags: javascript, map, indexing, optimization, performance
|
||||
---
|
||||
|
||||
## Build Index Maps for Repeated Lookups
|
||||
|
||||
Multiple `.find()` calls by the same key should use a Map.
|
||||
|
||||
**Incorrect (O(n) per lookup):**
|
||||
|
||||
```typescript
|
||||
function processOrders(orders: Order[], users: User[]) {
|
||||
return orders.map(order => ({
|
||||
...order,
|
||||
user: users.find(u => u.id === order.userId)
|
||||
}))
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (O(1) per lookup):**
|
||||
|
||||
```typescript
|
||||
function processOrders(orders: Order[], users: User[]) {
|
||||
const userById = new Map(users.map(u => [u.id, u]))
|
||||
|
||||
return orders.map(order => ({
|
||||
...order,
|
||||
user: userById.get(order.userId)
|
||||
}))
|
||||
}
|
||||
```
|
||||
|
||||
Build map once (O(n)), then all lookups are O(1).
|
||||
For 1000 orders × 1000 users: 1M ops → 2K ops.
|
||||
@@ -0,0 +1,49 @@
|
||||
---
|
||||
title: Early Length Check for Array Comparisons
|
||||
impact: MEDIUM-HIGH
|
||||
impactDescription: avoids expensive operations when lengths differ
|
||||
tags: javascript, arrays, performance, optimization, comparison
|
||||
---
|
||||
|
||||
## Early Length Check for Array Comparisons
|
||||
|
||||
When comparing arrays with expensive operations (sorting, deep equality, serialization), check lengths first. If lengths differ, the arrays cannot be equal.
|
||||
|
||||
In real-world applications, this optimization is especially valuable when the comparison runs in hot paths (event handlers, render loops).
|
||||
|
||||
**Incorrect (always runs expensive comparison):**
|
||||
|
||||
```typescript
|
||||
function hasChanges(current: string[], original: string[]) {
|
||||
// Always sorts and joins, even when lengths differ
|
||||
return current.sort().join() !== original.sort().join()
|
||||
}
|
||||
```
|
||||
|
||||
Two O(n log n) sorts run even when `current.length` is 5 and `original.length` is 100. There is also overhead of joining the arrays and comparing the strings.
|
||||
|
||||
**Correct (O(1) length check first):**
|
||||
|
||||
```typescript
|
||||
function hasChanges(current: string[], original: string[]) {
|
||||
// Early return if lengths differ
|
||||
if (current.length !== original.length) {
|
||||
return true
|
||||
}
|
||||
// Only sort/join when lengths match
|
||||
const currentSorted = current.toSorted()
|
||||
const originalSorted = original.toSorted()
|
||||
for (let i = 0; i < currentSorted.length; i++) {
|
||||
if (currentSorted[i] !== originalSorted[i]) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
```
|
||||
|
||||
This new approach is more efficient because:
|
||||
- It avoids the overhead of sorting and joining the arrays when lengths differ
|
||||
- It avoids consuming memory for the joined strings (especially important for large arrays)
|
||||
- It avoids mutating the original arrays
|
||||
- It returns early when a difference is found
|
||||
@@ -0,0 +1,82 @@
|
||||
---
|
||||
title: Use Loop for Min/Max Instead of Sort
|
||||
impact: LOW
|
||||
impactDescription: O(n) instead of O(n log n)
|
||||
tags: javascript, arrays, performance, sorting, algorithms
|
||||
---
|
||||
|
||||
## Use Loop for Min/Max Instead of Sort
|
||||
|
||||
Finding the smallest or largest element only requires a single pass through the array. Sorting is wasteful and slower.
|
||||
|
||||
**Incorrect (O(n log n) - sort to find latest):**
|
||||
|
||||
```typescript
|
||||
interface Project {
|
||||
id: string
|
||||
name: string
|
||||
updatedAt: number
|
||||
}
|
||||
|
||||
function getLatestProject(projects: Project[]) {
|
||||
const sorted = [...projects].sort((a, b) => b.updatedAt - a.updatedAt)
|
||||
return sorted[0]
|
||||
}
|
||||
```
|
||||
|
||||
Sorts the entire array just to find the maximum value.
|
||||
|
||||
**Incorrect (O(n log n) - sort for oldest and newest):**
|
||||
|
||||
```typescript
|
||||
function getOldestAndNewest(projects: Project[]) {
|
||||
const sorted = [...projects].sort((a, b) => a.updatedAt - b.updatedAt)
|
||||
return { oldest: sorted[0], newest: sorted[sorted.length - 1] }
|
||||
}
|
||||
```
|
||||
|
||||
Still sorts unnecessarily when only min/max are needed.
|
||||
|
||||
**Correct (O(n) - single loop):**
|
||||
|
||||
```typescript
|
||||
function getLatestProject(projects: Project[]) {
|
||||
if (projects.length === 0) return null
|
||||
|
||||
let latest = projects[0]
|
||||
|
||||
for (let i = 1; i < projects.length; i++) {
|
||||
if (projects[i].updatedAt > latest.updatedAt) {
|
||||
latest = projects[i]
|
||||
}
|
||||
}
|
||||
|
||||
return latest
|
||||
}
|
||||
|
||||
function getOldestAndNewest(projects: Project[]) {
|
||||
if (projects.length === 0) return { oldest: null, newest: null }
|
||||
|
||||
let oldest = projects[0]
|
||||
let newest = projects[0]
|
||||
|
||||
for (let i = 1; i < projects.length; i++) {
|
||||
if (projects[i].updatedAt < oldest.updatedAt) oldest = projects[i]
|
||||
if (projects[i].updatedAt > newest.updatedAt) newest = projects[i]
|
||||
}
|
||||
|
||||
return { oldest, newest }
|
||||
}
|
||||
```
|
||||
|
||||
Single pass through the array, no copying, no sorting.
|
||||
|
||||
**Alternative (Math.min/Math.max for small arrays):**
|
||||
|
||||
```typescript
|
||||
const numbers = [5, 2, 8, 1, 9]
|
||||
const min = Math.min(...numbers)
|
||||
const max = Math.max(...numbers)
|
||||
```
|
||||
|
||||
This works for small arrays but can be slower for very large arrays due to spread operator limitations. Use the loop approach for reliability.
|
||||
@@ -0,0 +1,24 @@
|
||||
---
|
||||
title: Use Set/Map for O(1) Lookups
|
||||
impact: LOW-MEDIUM
|
||||
impactDescription: O(n) to O(1)
|
||||
tags: javascript, set, map, data-structures, performance
|
||||
---
|
||||
|
||||
## Use Set/Map for O(1) Lookups
|
||||
|
||||
Convert arrays to Set/Map for repeated membership checks.
|
||||
|
||||
**Incorrect (O(n) per check):**
|
||||
|
||||
```typescript
|
||||
const allowedIds = ['a', 'b', 'c', ...]
|
||||
items.filter(item => allowedIds.includes(item.id))
|
||||
```
|
||||
|
||||
**Correct (O(1) per check):**
|
||||
|
||||
```typescript
|
||||
const allowedIds = new Set(['a', 'b', 'c', ...])
|
||||
items.filter(item => allowedIds.has(item.id))
|
||||
```
|
||||
@@ -0,0 +1,57 @@
|
||||
---
|
||||
title: Use toSorted() Instead of sort() for Immutability
|
||||
impact: MEDIUM-HIGH
|
||||
impactDescription: prevents mutation bugs in React state
|
||||
tags: javascript, arrays, immutability, react, state, mutation
|
||||
---
|
||||
|
||||
## Use toSorted() Instead of sort() for Immutability
|
||||
|
||||
`.sort()` mutates the array in place, which can cause bugs with React state and props. Use `.toSorted()` to create a new sorted array without mutation.
|
||||
|
||||
**Incorrect (mutates original array):**
|
||||
|
||||
```typescript
|
||||
function UserList({ users }: { users: User[] }) {
|
||||
// Mutates the users prop array!
|
||||
const sorted = useMemo(
|
||||
() => users.sort((a, b) => a.name.localeCompare(b.name)),
|
||||
[users]
|
||||
)
|
||||
return <div>{sorted.map(renderUser)}</div>
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (creates new array):**
|
||||
|
||||
```typescript
|
||||
function UserList({ users }: { users: User[] }) {
|
||||
// Creates new sorted array, original unchanged
|
||||
const sorted = useMemo(
|
||||
() => users.toSorted((a, b) => a.name.localeCompare(b.name)),
|
||||
[users]
|
||||
)
|
||||
return <div>{sorted.map(renderUser)}</div>
|
||||
}
|
||||
```
|
||||
|
||||
**Why this matters in React:**
|
||||
|
||||
1. Props/state mutations break React's immutability model - React expects props and state to be treated as read-only
|
||||
2. Causes stale closure bugs - Mutating arrays inside closures (callbacks, effects) can lead to unexpected behavior
|
||||
|
||||
**Browser support (fallback for older browsers):**
|
||||
|
||||
`.toSorted()` is available in all modern browsers (Chrome 110+, Safari 16+, Firefox 115+, Node.js 20+). For older environments, use spread operator:
|
||||
|
||||
```typescript
|
||||
// Fallback for older browsers
|
||||
const sorted = [...items].sort((a, b) => a.value - b.value)
|
||||
```
|
||||
|
||||
**Other immutable array methods:**
|
||||
|
||||
- `.toSorted()` - immutable sort
|
||||
- `.toReversed()` - immutable reverse
|
||||
- `.toSpliced()` - immutable splice
|
||||
- `.with()` - immutable element replacement
|
||||
@@ -0,0 +1,26 @@
|
||||
---
|
||||
title: Use Activity Component for Show/Hide
|
||||
impact: MEDIUM
|
||||
impactDescription: preserves state/DOM
|
||||
tags: rendering, activity, visibility, state-preservation
|
||||
---
|
||||
|
||||
## Use Activity Component for Show/Hide
|
||||
|
||||
Use React's `<Activity>` to preserve state/DOM for expensive components that frequently toggle visibility.
|
||||
|
||||
**Usage:**
|
||||
|
||||
```tsx
|
||||
import { Activity } from 'react'
|
||||
|
||||
function Dropdown({ isOpen }: Props) {
|
||||
return (
|
||||
<Activity mode={isOpen ? 'visible' : 'hidden'}>
|
||||
<ExpensiveMenu />
|
||||
</Activity>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
Avoids expensive re-renders and state loss.
|
||||
@@ -0,0 +1,47 @@
|
||||
---
|
||||
title: Animate SVG Wrapper Instead of SVG Element
|
||||
impact: LOW
|
||||
impactDescription: enables hardware acceleration
|
||||
tags: rendering, svg, css, animation, performance
|
||||
---
|
||||
|
||||
## Animate SVG Wrapper Instead of SVG Element
|
||||
|
||||
Many browsers don't have hardware acceleration for CSS3 animations on SVG elements. Wrap SVG in a `<div>` and animate the wrapper instead.
|
||||
|
||||
**Incorrect (animating SVG directly - no hardware acceleration):**
|
||||
|
||||
```tsx
|
||||
function LoadingSpinner() {
|
||||
return (
|
||||
<svg
|
||||
className="animate-spin"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<circle cx="12" cy="12" r="10" stroke="currentColor" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (animating wrapper div - hardware accelerated):**
|
||||
|
||||
```tsx
|
||||
function LoadingSpinner() {
|
||||
return (
|
||||
<div className="animate-spin">
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<circle cx="12" cy="12" r="10" stroke="currentColor" />
|
||||
</svg>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
This applies to all CSS transforms and transitions (`transform`, `opacity`, `translate`, `scale`, `rotate`). The wrapper div allows browsers to use GPU acceleration for smoother animations.
|
||||
@@ -0,0 +1,40 @@
|
||||
---
|
||||
title: Use Explicit Conditional Rendering
|
||||
impact: LOW
|
||||
impactDescription: prevents rendering 0 or NaN
|
||||
tags: rendering, conditional, jsx, falsy-values
|
||||
---
|
||||
|
||||
## Use Explicit Conditional Rendering
|
||||
|
||||
Use explicit ternary operators (`? :`) instead of `&&` for conditional rendering when the condition can be `0`, `NaN`, or other falsy values that render.
|
||||
|
||||
**Incorrect (renders "0" when count is 0):**
|
||||
|
||||
```tsx
|
||||
function Badge({ count }: { count: number }) {
|
||||
return (
|
||||
<div>
|
||||
{count && <span className="badge">{count}</span>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// When count = 0, renders: <div>0</div>
|
||||
// When count = 5, renders: <div><span class="badge">5</span></div>
|
||||
```
|
||||
|
||||
**Correct (renders nothing when count is 0):**
|
||||
|
||||
```tsx
|
||||
function Badge({ count }: { count: number }) {
|
||||
return (
|
||||
<div>
|
||||
{count > 0 ? <span className="badge">{count}</span> : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// When count = 0, renders: <div></div>
|
||||
// When count = 5, renders: <div><span class="badge">5</span></div>
|
||||
```
|
||||
@@ -0,0 +1,38 @@
|
||||
---
|
||||
title: CSS content-visibility for Long Lists
|
||||
impact: HIGH
|
||||
impactDescription: faster initial render
|
||||
tags: rendering, css, content-visibility, long-lists
|
||||
---
|
||||
|
||||
## CSS content-visibility for Long Lists
|
||||
|
||||
Apply `content-visibility: auto` to defer off-screen rendering.
|
||||
|
||||
**CSS:**
|
||||
|
||||
```css
|
||||
.message-item {
|
||||
content-visibility: auto;
|
||||
contain-intrinsic-size: 0 80px;
|
||||
}
|
||||
```
|
||||
|
||||
**Example:**
|
||||
|
||||
```tsx
|
||||
function MessageList({ messages }: { messages: Message[] }) {
|
||||
return (
|
||||
<div className="overflow-y-auto h-screen">
|
||||
{messages.map(msg => (
|
||||
<div key={msg.id} className="message-item">
|
||||
<Avatar user={msg.author} />
|
||||
<div>{msg.content}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
For 1000 messages, browser skips layout/paint for ~990 off-screen items (10× faster initial render).
|
||||
@@ -0,0 +1,46 @@
|
||||
---
|
||||
title: Hoist Static JSX Elements
|
||||
impact: LOW
|
||||
impactDescription: avoids re-creation
|
||||
tags: rendering, jsx, static, optimization
|
||||
---
|
||||
|
||||
## Hoist Static JSX Elements
|
||||
|
||||
Extract static JSX outside components to avoid re-creation.
|
||||
|
||||
**Incorrect (recreates element every render):**
|
||||
|
||||
```tsx
|
||||
function LoadingSkeleton() {
|
||||
return <div className="animate-pulse h-20 bg-gray-200" />
|
||||
}
|
||||
|
||||
function Container() {
|
||||
return (
|
||||
<div>
|
||||
{loading && <LoadingSkeleton />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (reuses same element):**
|
||||
|
||||
```tsx
|
||||
const loadingSkeleton = (
|
||||
<div className="animate-pulse h-20 bg-gray-200" />
|
||||
)
|
||||
|
||||
function Container() {
|
||||
return (
|
||||
<div>
|
||||
{loading && loadingSkeleton}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
This is especially helpful for large and static SVG nodes, which can be expensive to recreate on every render.
|
||||
|
||||
**Note:** If your project has [React Compiler](https://react.dev/learn/react-compiler) enabled, the compiler automatically hoists static JSX elements and optimizes component re-renders, making manual hoisting unnecessary.
|
||||
@@ -0,0 +1,82 @@
|
||||
---
|
||||
title: Prevent Hydration Mismatch Without Flickering
|
||||
impact: MEDIUM
|
||||
impactDescription: avoids visual flicker and hydration errors
|
||||
tags: rendering, ssr, hydration, localStorage, flicker
|
||||
---
|
||||
|
||||
## Prevent Hydration Mismatch Without Flickering
|
||||
|
||||
When rendering content that depends on client-side storage (localStorage, cookies), avoid both SSR breakage and post-hydration flickering by injecting a synchronous script that updates the DOM before React hydrates.
|
||||
|
||||
**Incorrect (breaks SSR):**
|
||||
|
||||
```tsx
|
||||
function ThemeWrapper({ children }: { children: ReactNode }) {
|
||||
// localStorage is not available on server - throws error
|
||||
const theme = localStorage.getItem('theme') || 'light'
|
||||
|
||||
return (
|
||||
<div className={theme}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
Server-side rendering will fail because `localStorage` is undefined.
|
||||
|
||||
**Incorrect (visual flickering):**
|
||||
|
||||
```tsx
|
||||
function ThemeWrapper({ children }: { children: ReactNode }) {
|
||||
const [theme, setTheme] = useState('light')
|
||||
|
||||
useEffect(() => {
|
||||
// Runs after hydration - causes visible flash
|
||||
const stored = localStorage.getItem('theme')
|
||||
if (stored) {
|
||||
setTheme(stored)
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className={theme}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
Component first renders with default value (`light`), then updates after hydration, causing a visible flash of incorrect content.
|
||||
|
||||
**Correct (no flicker, no hydration mismatch):**
|
||||
|
||||
```tsx
|
||||
function ThemeWrapper({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<>
|
||||
<div id="theme-wrapper">
|
||||
{children}
|
||||
</div>
|
||||
<script
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
(function() {
|
||||
try {
|
||||
var theme = localStorage.getItem('theme') || 'light';
|
||||
var el = document.getElementById('theme-wrapper');
|
||||
if (el) el.className = theme;
|
||||
} catch (e) {}
|
||||
})();
|
||||
`,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
The inline script executes synchronously before showing the element, ensuring the DOM already has the correct value. No flickering, no hydration mismatch.
|
||||
|
||||
This pattern is especially useful for theme toggles, user preferences, authentication states, and any client-only data that should render immediately without flashing default values.
|
||||
@@ -0,0 +1,28 @@
|
||||
---
|
||||
title: Optimize SVG Precision
|
||||
impact: LOW
|
||||
impactDescription: reduces file size
|
||||
tags: rendering, svg, optimization, svgo
|
||||
---
|
||||
|
||||
## Optimize SVG Precision
|
||||
|
||||
Reduce SVG coordinate precision to decrease file size. The optimal precision depends on the viewBox size, but in general reducing precision should be considered.
|
||||
|
||||
**Incorrect (excessive precision):**
|
||||
|
||||
```svg
|
||||
<path d="M 10.293847 20.847362 L 30.938472 40.192837" />
|
||||
```
|
||||
|
||||
**Correct (1 decimal place):**
|
||||
|
||||
```svg
|
||||
<path d="M 10.3 20.8 L 30.9 40.2" />
|
||||
```
|
||||
|
||||
**Automate with SVGO:**
|
||||
|
||||
```bash
|
||||
npx svgo --precision=1 --multipass icon.svg
|
||||
```
|
||||
@@ -0,0 +1,39 @@
|
||||
---
|
||||
title: Defer State Reads to Usage Point
|
||||
impact: MEDIUM
|
||||
impactDescription: avoids unnecessary subscriptions
|
||||
tags: rerender, searchParams, localStorage, optimization
|
||||
---
|
||||
|
||||
## Defer State Reads to Usage Point
|
||||
|
||||
Don't subscribe to dynamic state (searchParams, localStorage) if you only read it inside callbacks.
|
||||
|
||||
**Incorrect (subscribes to all searchParams changes):**
|
||||
|
||||
```tsx
|
||||
function ShareButton({ chatId }: { chatId: string }) {
|
||||
const searchParams = useSearchParams()
|
||||
|
||||
const handleShare = () => {
|
||||
const ref = searchParams.get('ref')
|
||||
shareChat(chatId, { ref })
|
||||
}
|
||||
|
||||
return <button onClick={handleShare}>Share</button>
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (reads on demand, no subscription):**
|
||||
|
||||
```tsx
|
||||
function ShareButton({ chatId }: { chatId: string }) {
|
||||
const handleShare = () => {
|
||||
const params = new URLSearchParams(window.location.search)
|
||||
const ref = params.get('ref')
|
||||
shareChat(chatId, { ref })
|
||||
}
|
||||
|
||||
return <button onClick={handleShare}>Share</button>
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,45 @@
|
||||
---
|
||||
title: Narrow Effect Dependencies
|
||||
impact: LOW
|
||||
impactDescription: minimizes effect re-runs
|
||||
tags: rerender, useEffect, dependencies, optimization
|
||||
---
|
||||
|
||||
## Narrow Effect Dependencies
|
||||
|
||||
Specify primitive dependencies instead of objects to minimize effect re-runs.
|
||||
|
||||
**Incorrect (re-runs on any user field change):**
|
||||
|
||||
```tsx
|
||||
useEffect(() => {
|
||||
fetchUserDetails(user.id)
|
||||
}, [user])
|
||||
```
|
||||
|
||||
**Correct (re-runs only when id changes):**
|
||||
|
||||
```tsx
|
||||
useEffect(() => {
|
||||
fetchUserDetails(user.id)
|
||||
}, [user.id])
|
||||
```
|
||||
|
||||
**For derived state, compute outside effect:**
|
||||
|
||||
```tsx
|
||||
// Incorrect: runs on width=767, 766, 765...
|
||||
useEffect(() => {
|
||||
if (width < 768) {
|
||||
enableMobileMode()
|
||||
}
|
||||
}, [width])
|
||||
|
||||
// Correct: runs only on boolean transition
|
||||
const isMobile = width < 768
|
||||
useEffect(() => {
|
||||
if (isMobile) {
|
||||
enableMobileMode()
|
||||
}
|
||||
}, [isMobile])
|
||||
```
|
||||
@@ -0,0 +1,29 @@
|
||||
---
|
||||
title: Subscribe to Derived State
|
||||
impact: MEDIUM
|
||||
impactDescription: reduces re-render frequency
|
||||
tags: rerender, derived-state, media-query, optimization
|
||||
---
|
||||
|
||||
## Subscribe to Derived State
|
||||
|
||||
Subscribe to derived boolean state instead of continuous values to reduce re-render frequency.
|
||||
|
||||
**Incorrect (re-renders on every pixel change):**
|
||||
|
||||
```tsx
|
||||
function Sidebar() {
|
||||
const width = useWindowWidth() // updates continuously
|
||||
const isMobile = width < 768
|
||||
return <nav className={isMobile ? 'mobile' : 'desktop'}></nav>
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (re-renders only when boolean changes):**
|
||||
|
||||
```tsx
|
||||
function Sidebar() {
|
||||
const isMobile = useMediaQuery('(max-width: 767px)')
|
||||
return <nav className={isMobile ? 'mobile' : 'desktop'}></nav>
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,74 @@
|
||||
---
|
||||
title: Use Functional setState Updates
|
||||
impact: MEDIUM
|
||||
impactDescription: prevents stale closures and unnecessary callback recreations
|
||||
tags: react, hooks, useState, useCallback, callbacks, closures
|
||||
---
|
||||
|
||||
## Use Functional setState Updates
|
||||
|
||||
When updating state based on the current state value, use the functional update form of setState instead of directly referencing the state variable. This prevents stale closures, eliminates unnecessary dependencies, and creates stable callback references.
|
||||
|
||||
**Incorrect (requires state as dependency):**
|
||||
|
||||
```tsx
|
||||
function TodoList() {
|
||||
const [items, setItems] = useState(initialItems)
|
||||
|
||||
// Callback must depend on items, recreated on every items change
|
||||
const addItems = useCallback((newItems: Item[]) => {
|
||||
setItems([...items, ...newItems])
|
||||
}, [items]) // ❌ items dependency causes recreations
|
||||
|
||||
// Risk of stale closure if dependency is forgotten
|
||||
const removeItem = useCallback((id: string) => {
|
||||
setItems(items.filter(item => item.id !== id))
|
||||
}, []) // ❌ Missing items dependency - will use stale items!
|
||||
|
||||
return <ItemsEditor items={items} onAdd={addItems} onRemove={removeItem} />
|
||||
}
|
||||
```
|
||||
|
||||
The first callback is recreated every time `items` changes, which can cause child components to re-render unnecessarily. The second callback has a stale closure bug—it will always reference the initial `items` value.
|
||||
|
||||
**Correct (stable callbacks, no stale closures):**
|
||||
|
||||
```tsx
|
||||
function TodoList() {
|
||||
const [items, setItems] = useState(initialItems)
|
||||
|
||||
// Stable callback, never recreated
|
||||
const addItems = useCallback((newItems: Item[]) => {
|
||||
setItems(curr => [...curr, ...newItems])
|
||||
}, []) // ✅ No dependencies needed
|
||||
|
||||
// Always uses latest state, no stale closure risk
|
||||
const removeItem = useCallback((id: string) => {
|
||||
setItems(curr => curr.filter(item => item.id !== id))
|
||||
}, []) // ✅ Safe and stable
|
||||
|
||||
return <ItemsEditor items={items} onAdd={addItems} onRemove={removeItem} />
|
||||
}
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
|
||||
1. **Stable callback references** - Callbacks don't need to be recreated when state changes
|
||||
2. **No stale closures** - Always operates on the latest state value
|
||||
3. **Fewer dependencies** - Simplifies dependency arrays and reduces memory leaks
|
||||
4. **Prevents bugs** - Eliminates the most common source of React closure bugs
|
||||
|
||||
**When to use functional updates:**
|
||||
|
||||
- Any setState that depends on the current state value
|
||||
- Inside useCallback/useMemo when state is needed
|
||||
- Event handlers that reference state
|
||||
- Async operations that update state
|
||||
|
||||
**When direct updates are fine:**
|
||||
|
||||
- Setting state to a static value: `setCount(0)`
|
||||
- Setting state from props/arguments only: `setName(newName)`
|
||||
- State doesn't depend on previous value
|
||||
|
||||
**Note:** If your project has [React Compiler](https://react.dev/learn/react-compiler) enabled, the compiler can automatically optimize some cases, but functional updates are still recommended for correctness and to prevent stale closure bugs.
|
||||
@@ -0,0 +1,58 @@
|
||||
---
|
||||
title: Use Lazy State Initialization
|
||||
impact: MEDIUM
|
||||
impactDescription: wasted computation on every render
|
||||
tags: react, hooks, useState, performance, initialization
|
||||
---
|
||||
|
||||
## Use Lazy State Initialization
|
||||
|
||||
Pass a function to `useState` for expensive initial values. Without the function form, the initializer runs on every render even though the value is only used once.
|
||||
|
||||
**Incorrect (runs on every render):**
|
||||
|
||||
```tsx
|
||||
function FilteredList({ items }: { items: Item[] }) {
|
||||
// buildSearchIndex() runs on EVERY render, even after initialization
|
||||
const [searchIndex, setSearchIndex] = useState(buildSearchIndex(items))
|
||||
const [query, setQuery] = useState('')
|
||||
|
||||
// When query changes, buildSearchIndex runs again unnecessarily
|
||||
return <SearchResults index={searchIndex} query={query} />
|
||||
}
|
||||
|
||||
function UserProfile() {
|
||||
// JSON.parse runs on every render
|
||||
const [settings, setSettings] = useState(
|
||||
JSON.parse(localStorage.getItem('settings') || '{}')
|
||||
)
|
||||
|
||||
return <SettingsForm settings={settings} onChange={setSettings} />
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (runs only once):**
|
||||
|
||||
```tsx
|
||||
function FilteredList({ items }: { items: Item[] }) {
|
||||
// buildSearchIndex() runs ONLY on initial render
|
||||
const [searchIndex, setSearchIndex] = useState(() => buildSearchIndex(items))
|
||||
const [query, setQuery] = useState('')
|
||||
|
||||
return <SearchResults index={searchIndex} query={query} />
|
||||
}
|
||||
|
||||
function UserProfile() {
|
||||
// JSON.parse runs only on initial render
|
||||
const [settings, setSettings] = useState(() => {
|
||||
const stored = localStorage.getItem('settings')
|
||||
return stored ? JSON.parse(stored) : {}
|
||||
})
|
||||
|
||||
return <SettingsForm settings={settings} onChange={setSettings} />
|
||||
}
|
||||
```
|
||||
|
||||
Use lazy initialization when computing initial values from localStorage/sessionStorage, building data structures (indexes, maps), reading from the DOM, or performing heavy transformations.
|
||||
|
||||
For simple primitives (`useState(0)`), direct references (`useState(props.value)`), or cheap literals (`useState({})`), the function form is unnecessary.
|
||||
@@ -0,0 +1,44 @@
|
||||
---
|
||||
title: Extract to Memoized Components
|
||||
impact: MEDIUM
|
||||
impactDescription: enables early returns
|
||||
tags: rerender, memo, useMemo, optimization
|
||||
---
|
||||
|
||||
## Extract to Memoized Components
|
||||
|
||||
Extract expensive work into memoized components to enable early returns before computation.
|
||||
|
||||
**Incorrect (computes avatar even when loading):**
|
||||
|
||||
```tsx
|
||||
function Profile({ user, loading }: Props) {
|
||||
const avatar = useMemo(() => {
|
||||
const id = computeAvatarId(user)
|
||||
return <Avatar id={id} />
|
||||
}, [user])
|
||||
|
||||
if (loading) return <Skeleton />
|
||||
return <div>{avatar}</div>
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (skips computation when loading):**
|
||||
|
||||
```tsx
|
||||
const UserAvatar = memo(function UserAvatar({ user }: { user: User }) {
|
||||
const id = useMemo(() => computeAvatarId(user), [user])
|
||||
return <Avatar id={id} />
|
||||
})
|
||||
|
||||
function Profile({ user, loading }: Props) {
|
||||
if (loading) return <Skeleton />
|
||||
return (
|
||||
<div>
|
||||
<UserAvatar user={user} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
**Note:** If your project has [React Compiler](https://react.dev/learn/react-compiler) enabled, manual memoization with `memo()` and `useMemo()` is not necessary. The compiler automatically optimizes re-renders.
|
||||
@@ -0,0 +1,40 @@
|
||||
---
|
||||
title: Use Transitions for Non-Urgent Updates
|
||||
impact: MEDIUM
|
||||
impactDescription: maintains UI responsiveness
|
||||
tags: rerender, transitions, startTransition, performance
|
||||
---
|
||||
|
||||
## Use Transitions for Non-Urgent Updates
|
||||
|
||||
Mark frequent, non-urgent state updates as transitions to maintain UI responsiveness.
|
||||
|
||||
**Incorrect (blocks UI on every scroll):**
|
||||
|
||||
```tsx
|
||||
function ScrollTracker() {
|
||||
const [scrollY, setScrollY] = useState(0)
|
||||
useEffect(() => {
|
||||
const handler = () => setScrollY(window.scrollY)
|
||||
window.addEventListener('scroll', handler, { passive: true })
|
||||
return () => window.removeEventListener('scroll', handler)
|
||||
}, [])
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (non-blocking updates):**
|
||||
|
||||
```tsx
|
||||
import { startTransition } from 'react'
|
||||
|
||||
function ScrollTracker() {
|
||||
const [scrollY, setScrollY] = useState(0)
|
||||
useEffect(() => {
|
||||
const handler = () => {
|
||||
startTransition(() => setScrollY(window.scrollY))
|
||||
}
|
||||
window.addEventListener('scroll', handler, { passive: true })
|
||||
return () => window.removeEventListener('scroll', handler)
|
||||
}, [])
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,72 @@
|
||||
---
|
||||
title: Use after() for Non-Blocking Operations
|
||||
impact: MEDIUM
|
||||
impactDescription: faster response times
|
||||
tags: server, async, logging, analytics, side-effects
|
||||
---
|
||||
|
||||
## Use after() for Non-Blocking Operations
|
||||
|
||||
Use Next.js's `after()` to schedule work that should execute after a response is sent. This prevents logging, analytics, and other side effects from blocking the response.
|
||||
|
||||
**Incorrect (blocks response):**
|
||||
|
||||
```tsx
|
||||
import { logUserAction } from '@/app/utils'
|
||||
|
||||
export async function POST(request: Request) {
|
||||
// Perform mutation
|
||||
await updateDatabase(request)
|
||||
|
||||
// Logging blocks the response
|
||||
const userAgent = request.headers.get('user-agent') || 'unknown'
|
||||
await logUserAction({ userAgent })
|
||||
|
||||
return new Response(JSON.stringify({ status: 'success' }), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (non-blocking):**
|
||||
|
||||
```tsx
|
||||
import { after } from 'next/server'
|
||||
import { headers } from 'next/headers'
|
||||
import { logUserAction } from '@/app/utils'
|
||||
|
||||
export async function POST(request: Request) {
|
||||
// Perform mutation
|
||||
await updateDatabase(request)
|
||||
|
||||
// Log after response is sent
|
||||
after(async () => {
|
||||
const userAgent = (await headers()).get('user-agent') || 'unknown'
|
||||
|
||||
await logUserAction({ userAgent })
|
||||
})
|
||||
|
||||
return new Response(JSON.stringify({ status: 'success' }), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
The response is sent immediately while logging happens in the background.
|
||||
|
||||
**Common use cases:**
|
||||
|
||||
- Analytics tracking
|
||||
- Audit logging
|
||||
- Sending notifications
|
||||
- Cache invalidation
|
||||
- Cleanup tasks
|
||||
|
||||
**Important notes:**
|
||||
|
||||
- `after()` runs even if the response fails or redirects
|
||||
- Works in Server Actions, Route Handlers, and Server Components
|
||||
|
||||
Reference: [https://nextjs.org/docs/app/api-reference/functions/after](https://nextjs.org/docs/app/api-reference/functions/after)
|
||||
@@ -0,0 +1,41 @@
|
||||
---
|
||||
title: Cross-Request LRU Caching
|
||||
impact: HIGH
|
||||
impactDescription: caches across requests
|
||||
tags: server, cache, lru, cross-request
|
||||
---
|
||||
|
||||
## Cross-Request LRU Caching
|
||||
|
||||
`React.cache()` only works within one request. For data shared across sequential requests (user clicks button A then button B), use an LRU cache.
|
||||
|
||||
**Implementation:**
|
||||
|
||||
```typescript
|
||||
import { LRUCache } from 'lru-cache'
|
||||
|
||||
const cache = new LRUCache<string, any>({
|
||||
max: 1000,
|
||||
ttl: 5 * 60 * 1000 // 5 minutes
|
||||
})
|
||||
|
||||
export async function getUser(id: string) {
|
||||
const cached = cache.get(id)
|
||||
if (cached) return cached
|
||||
|
||||
const user = await db.user.findUnique({ where: { id } })
|
||||
cache.set(id, user)
|
||||
return user
|
||||
}
|
||||
|
||||
// Request 1: DB query, result cached
|
||||
// Request 2: cache hit, no DB query
|
||||
```
|
||||
|
||||
Use when sequential user actions hit multiple endpoints needing the same data within seconds.
|
||||
|
||||
**With Vercel's [Fluid Compute](https://vercel.com/docs/fluid-compute):** LRU caching is especially effective because multiple concurrent requests can share the same function instance and cache. This means the cache persists across requests without needing external storage like Redis.
|
||||
|
||||
**In traditional serverless:** Each invocation runs in isolation, so consider Redis for cross-process caching.
|
||||
|
||||
Reference: [https://github.com/isaacs/node-lru-cache](https://github.com/isaacs/node-lru-cache)
|
||||
@@ -0,0 +1,26 @@
|
||||
---
|
||||
title: Per-Request Deduplication with React.cache()
|
||||
impact: MEDIUM
|
||||
impactDescription: deduplicates within request
|
||||
tags: server, cache, react-cache, deduplication
|
||||
---
|
||||
|
||||
## Per-Request Deduplication with React.cache()
|
||||
|
||||
Use `React.cache()` for server-side request deduplication. Authentication and database queries benefit most.
|
||||
|
||||
**Usage:**
|
||||
|
||||
```typescript
|
||||
import { cache } from 'react'
|
||||
|
||||
export const getCurrentUser = cache(async () => {
|
||||
const session = await auth()
|
||||
if (!session?.user?.id) return null
|
||||
return await db.user.findUnique({
|
||||
where: { id: session.user.id }
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
Within a single request, multiple calls to `getCurrentUser()` execute the query only once.
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user