Skip to main content

Core Principles

As a Cal.com contributor, you should prioritize:
  1. Type Safety - Use TypeScript strictly, avoid any
  2. Security - Never expose sensitive data or credentials
  3. Small PRs - Keep changes focused and reviewable (<500 lines, <10 files)
  4. Code Quality - Write clean, maintainable code that explains “why”, not “what”

Do’s and Don’ts

Always Do

Use select instead of include in Prisma queries
Use import type { X } for TypeScript type imports
Use early returns to reduce nesting
Use ErrorWithCode for non-tRPC errors, TRPCError for tRPC routers
Use conventional commits: feat:, fix:, refactor:
Import directly from source files, not barrel files
Add translations to packages/i18n/locales/en/common.json
Use date-fns or native Date when timezone awareness isn’t needed
Put permission checks in page.tsx, never in layout.tsx
Use Biome for formatting and linting
Run yarn type-check:ci --force before pushing

Never Do

Never use as any - use proper type-safe solutions
Never expose credential.key field in API responses
Never commit secrets, API keys, or .env files
Never modify *.generated.ts files directly
Never put business logic in repositories
Never use barrel imports from index.ts files
Never skip running type checks before pushing
Never create large PRs (>500 lines or >10 files)
Never add comments that restate what code does

TypeScript Best Practices

Type Imports

// Good - Type imports
import type { User } from "@prisma/client";
import type { NextApiRequest, NextApiResponse } from "next";

// Bad - Regular imports for types
import { User } from "@prisma/client";

Avoid any

// Good - Proper typing
function processBooking(booking: Booking): BookingResponse {
  return { id: booking.id, status: booking.status };
}

// Bad - Using any
function processBooking(booking: any): any {
  return booking;
}

Use Early Returns

// Good - Early returns reduce nesting
function getBooking(id: string) {
  const booking = await prisma.booking.findUnique({ where: { id } });
  if (!booking) return null;
  
  const user = await prisma.user.findUnique({ where: { id: booking.userId } });
  if (!user) return null;
  
  return { ...booking, user };
}

// Bad - Nested conditions
function getBooking(id: string) {
  const booking = await prisma.booking.findUnique({ where: { id } });
  if (booking) {
    const user = await prisma.user.findUnique({ where: { id: booking.userId } });
    if (user) {
      return { ...booking, user };
    }
  }
  return null;
}

Prisma Best Practices

Use select Over include

// Good - Explicit field selection
const booking = await prisma.booking.findFirst({
  select: {
    id: true,
    title: true,
    startTime: true,
    user: {
      select: {
        id: true,
        name: true,
        email: true,
      },
    },
  },
});

// Bad - Includes all fields (performance and security issue)
const booking = await prisma.booking.findFirst({
  include: {
    user: true,
  },
});
Using select improves performance and prevents accidental exposure of sensitive data like credential.key.

Never Expose Credentials

// Good - Exclude sensitive fields
const credentials = await prisma.credential.findMany({
  select: {
    id: true,
    type: true,
    userId: true,
    // Never select credential.key
  },
});

// Bad - Exposes sensitive data
const credentials = await prisma.credential.findMany();

Import Best Practices

Direct Imports, Not Barrel Files

// Good - Direct imports
import { Button } from "@calcom/ui/components/button";
import { Dialog } from "@calcom/ui/components/dialog";

// Bad - Barrel imports (slower, breaks tree-shaking)
import { Button, Dialog } from "@calcom/ui";

API v2 Imports

When importing into apps/api/v2, re-export from platform libraries:
// Step 1: In packages/platform/libraries/index.ts
export { ProfileRepository } from "@calcom/features/profile/repositories/ProfileRepository";

// Step 2: In apps/api/v2, import from platform-libraries
import { ProfileRepository } from "@calcom/platform-libraries";

// Bad - Direct import causes module not found error
import { ProfileRepository } from "@calcom/features/profile/repositories/ProfileRepository";

Error Handling

Use Appropriate Error Classes

// Good - TRPCError in tRPC routers
import { TRPCError } from "@trpc/server";

throw new TRPCError({
  code: "NOT_FOUND",
  message: `Booking ${bookingId} not found for user ${userId}`,
});

// Good - ErrorWithCode in services/utilities
import { ErrorWithCode } from "@calcom/lib/error";

throw new ErrorWithCode(
  `Unable to create booking: User ${userId} has no available time slots for ${date}`,
  "BOOKING_UNAVAILABLE"
);

// Bad - Generic error without context
throw new Error("Booking failed");

Descriptive Error Messages

Always provide context in error messages:
// Good - Context included
throw new Error(
  `Unable to send email to ${email}: SMTP connection failed with code ${errorCode}`
);

// Bad - No context
throw new Error("Email failed");

File Naming Conventions

Services

// Pattern: <Entity>Service.ts

// File: MembershipService.ts
export class MembershipService { ... }

// File: HashedLinkService.ts
export class HashedLinkService { ... }

Repositories

// Pattern: Prisma<Entity>Repository.ts

// File: PrismaAppRepository.ts
export class PrismaAppRepository { ... }

// File: PrismaMembershipRepository.ts
export class PrismaMembershipRepository { ... }
  • File names must match exported class names exactly (PascalCase)
  • Avoid dot-suffixes like .service.ts or .repository.ts (legacy patterns)
  • Reserve suffixes for .test.ts, .spec.ts, and .types.ts

Code Comments

Only add comments that explain why, not what.
// Good - Explains why
// We need to delay the webhook by 5 seconds to ensure the booking
// is fully committed before external systems receive the notification
await delay(5000);

// Bad - Restates what the code does
// Get the user
const user = await getUser();

Internationalization

Add all UI strings to translation files:
// packages/i18n/locales/en/common.json
{
  "booking_confirmed": "Booking confirmed",
  "booking_cancelled": "Booking cancelled"
}

// Usage in components
import { useLocale } from "@calcom/lib/hooks/useLocale";

const { t } = useLocale();
return <div>{t("booking_confirmed")}</div>;

Conventional Commits

Use conventional commit format for PR titles:
feat(bookings): add cancellation reason field
fix(calendar): resolve timezone conversion bug
refactor(auth): extract session validation logic
docs(api): update webhook documentation
test(bookings): add unit tests for reschedule flow
Format: <type>(<scope>): <description> Types:
  • feat - New feature
  • fix - Bug fix
  • refactor - Code refactoring
  • docs - Documentation changes
  • test - Adding/updating tests
  • chore - Maintenance tasks

PR Size Guidelines

Keep PRs small and focused for faster reviews and easier debugging.

Size Limits

  • Lines changed: <500 lines of code
  • Files changed: <10 code files
  • Single responsibility: Each PR should do one thing well
These limits exclude documentation, lock files, and auto-generated files.

Splitting Large Changes

By layer:
PR 1: Database schema changes
PR 2: Backend API endpoints
PR 3: Frontend UI components
PR 4: Integration and testing
By feature component:
PR 1: Add notification preferences schema
PR 2: Add notification service and API
PR 3: Add notification UI components
PR 4: Integrate notifications into booking flow
By refactor vs feature:
PR 1: Extract calendar logic into service
PR 2: Add new calendar provider support

Testing Requirements

Before Committing

# Type check
yarn type-check:ci --force

# Lint and format
yarn biome check --write .

# Run relevant tests
TZ=UTC yarn test

Before Pushing

Security Guidelines

Never Commit Secrets

# Add to .gitignore
.env
.env.local
credentials.json

Validate User Input

import { z } from "zod";

// Good - Zod validation
const createBookingSchema = z.object({
  eventTypeId: z.number(),
  startTime: z.string().datetime(),
  timeZone: z.string(),
});

const validated = createBookingSchema.parse(input);

Sanitize Output

// Good - Only return necessary fields
return {
  id: user.id,
  name: user.name,
  email: user.email,
  // Never include password, tokens, or credential keys
};

Boundaries

Ask First

  • Adding new dependencies
  • Schema changes to packages/prisma/schema.prisma
  • Changes affecting multiple packages
  • Deleting files
  • Running full build or E2E suites

Never Do Without Permission

  • Commit secrets or API keys
  • Force push to shared branches
  • Modify generated files directly
  • Expose sensitive data in APIs

PR Checklist

Before submitting a PR:

Code Review Guidelines

As a Contributor

  • Respond to feedback promptly
  • Ask questions if feedback is unclear
  • Update your branch regularly
  • Test suggested changes before pushing

As a Reviewer

  • Be respectful and constructive
  • Explain the “why” behind suggestions
  • Approve when ready, don’t nitpick
  • Use “Request changes” sparingly

Resources

Next Steps

Build docs developers (and LLMs) love