Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/formbricks/formbricks/llms.txt

Use this file to discover all available pages before exploring further.

This guide outlines the coding standards and best practices for contributing to Formbricks.

General Principles

DRY

Don’t Repeat Yourself - Extract reusable logic

KISS

Keep It Simple, Stupid - Favor simplicity

SOLID

Follow SOLID principles for clean code

Clean Code

Write code that’s easy to read and maintain

TypeScript Standards

Type Safety

Use TypeScript for all code. Leverage the type system for safety.
import type { Survey } from "@formbricks/types/survey";

interface GetSurveyOptions {
  surveyId: string;
  environmentId: string;
}

const getSurvey = async ({ surveyId, environmentId }: GetSurveyOptions): Promise<Survey> => {
  // Implementation
};

Type Inference

Prefer type inference when the type is obvious.
const count = surveys.length; // Type inferred as number
const names = surveys.map(s => s.name); // Type inferred as string[]

Shared Types

Use types from @formbricks/types package:
import type { Survey } from "@formbricks/types/survey";
import type { Response } from "@formbricks/types/response";
import type { User } from "@formbricks/types/user";

Avoid any

Never use any. Use unknown if type is truly unknown:
const parseData = (data: unknown): Survey => {
  // Validate and parse
  return surveySchema.parse(data);
};

Naming Conventions

Variables and Functions

Use camelCase for variables and functions:
const surveyCount = 10;
const getUserById = (id: string) => { ... };

Components and Classes

Use PascalCase for React components and classes:
const SurveyList = () => { ... };
class SurveyService { ... }

Constants

Use SCREAMING_SNAKE_CASE only for true constants:
const MAX_SURVEY_LENGTH = 100;
const API_BASE_URL = "https://api.formbricks.com";

Files and Folders

  • Components: PascalCase.tsx (e.g., SurveyList.tsx)
  • Utilities: camelCase.ts (e.g., surveyUtils.ts)
  • Modules/Folders: kebab-case or camelCase (e.g., survey-editor/)

Meaningful Names

Use descriptive, self-documenting names:
const filteredActiveSurveys = surveys.filter(s => s.status === "active");
const calculateAverageResponseTime = (responses: Response[]) => { ... };

React Standards

Server vs Client Components

Use Server Components by default. Only use Client Components when needed.
// No "use client" directive
import { getSurveys } from "@/lib/survey/service";

const SurveyList = async () => {
  const surveys = await getSurveys();
  return <div>{/* Render surveys */}</div>;
};
Use Client Components for:
  • State management (useState, useReducer)
  • Effects (useEffect)
  • Event handlers (onClick, onChange)
  • Browser APIs
  • Third-party libraries requiring browser context

Component Structure

Follow a consistent component structure:
"use client"; // If needed

// 1. Imports
import { useState } from "react";
import type { Survey } from "@formbricks/types/survey";

// 2. Types/Interfaces
interface SurveyCardProps {
  survey: Survey;
  onDelete?: (id: string) => void;
}

// 3. Component
export const SurveyCard = ({ survey, onDelete }: SurveyCardProps) => {
  // 4. Hooks
  const [isDeleting, setIsDeleting] = useState(false);

  // 5. Handlers
  const handleDelete = async () => {
    setIsDeleting(true);
    await onDelete?.(survey.id);
    setIsDeleting(false);
  };

  // 6. Render
  return (
    <div>
      <h3>{survey.name}</h3>
      <button onClick={handleDelete} disabled={isDeleting}>
        Delete
      </button>
    </div>
  );
};

Hooks Rules

Follow React hooks rules:
  • Call hooks at the top level
  • Don’t call hooks conditionally
  • Use custom hooks to extract logic
const useSurvey = (surveyId: string) => {
  const [survey, setSurvey] = useState<Survey | null>(null);
  
  useEffect(() => {
    fetchSurvey(surveyId).then(setSurvey);
  }, [surveyId]);
  
  return survey;
};

Context Providers

Guard against missing provider usage:
import { createContext, useContext } from "react";

const SurveyContext = createContext<SurveyContextType | null>(null);

export const useSurveyContext = () => {
  const context = useContext(SurveyContext);
  if (!context) {
    throw new Error("useSurveyContext must be used within SurveyProvider");
  }
  return context;
};

useEffect Cleanup

Use cleanup patterns that snapshot refs:
useEffect(() => {
  const currentValue = someRef.current;
  
  return () => {
    // Use currentValue instead of someRef.current
    cleanup(currentValue);
  };
}, []);

Formatting

ESLint and Prettier

We use ESLint and Prettier for consistent formatting:
# Format code
pnpm format

# Lint and fix
pnpm lint

Code Style

  • Indentation: 2 spaces (no tabs)
  • Line Length: 110 characters max
  • Quotes: Double quotes
  • Semicolons: Always use semicolons
  • Trailing Commas: Use in multi-line

Import Organization

Organize imports in this order:
// 1. External libraries
import { useState } from "react";
import { prisma } from "@formbricks/database";

// 2. Internal modules
import { getSurvey } from "@/lib/survey/service";
import { Button } from "@/components/ui/Button";

// 3. Types
import type { Survey } from "@formbricks/types/survey";

// 4. Relative imports
import { SurveyCard } from "./SurveyCard";
import styles from "./styles.module.css";

Error Handling

Server Actions

Return consistent response format:
import { z } from "zod";

const schema = z.object({
  name: z.string(),
});

export const createSurvey = async (data: unknown) => {
  try {
    const validated = schema.parse(data);
    const survey = await surveyService.create(validated);
    return { data: survey };
  } catch (error) {
    return { error: "Failed to create survey" };
  }
};

Error Boundaries

Use error boundaries for React errors:
// app/error.tsx
"use client";

export default function Error({
  error,
  reset,
}: {
  error: Error & { digest?: string };
  reset: () => void;
}) {
  return (
    <div>
      <h2>Something went wrong!</h2>
      <button onClick={reset}>Try again</button>
    </div>
  );
}

Validation with Zod

Use Zod for runtime validation:
import { z } from "zod";

const surveySchema = z.object({
  name: z.string().min(1).max(100),
  status: z.enum(["draft", "active", "completed"]),
  questions: z.array(questionSchema),
});

type Survey = z.infer<typeof surveySchema>;

Database & Prisma

Multi-Tenancy

Always scope queries by Organization or Environment:
const surveys = await prisma.survey.findMany({
  where: {
    environmentId,
    isActive: true,
  },
});

Soft Deletion

Check for soft deletion fields:
const activeSurveys = await prisma.survey.findMany({
  where: {
    environmentId,
    isActive: true,
    deletedAt: null,
  },
});

Performance

Never use skip/offset with count():
const [surveys, count] = await Promise.all([
  prisma.survey.findMany({
    where: { environmentId },
    take: 10,
  }),
  prisma.survey.count({
    where: { environmentId },
  }),
]);

Cursor Pagination

Use cursor-based pagination for large datasets:
const responses = await prisma.response.findMany({
  where: { surveyId },
  take: 50,
  cursor: lastId ? { id: lastId } : undefined,
  orderBy: { createdAt: "desc" },
});

Caching

React Cache

Use React cache() for request-level deduplication:
import { cache } from "react";

export const getSurvey = cache(async (surveyId: string) => {
  return await prisma.survey.findUnique({
    where: { id: surveyId },
  });
});

Redis Cache

Use Redis for expensive operations:
import { cache } from "@formbricks/cache";
import { createCacheKey } from "@/lib/cache";

const cacheKey = createCacheKey.survey(surveyId);
const cached = await cache.get(cacheKey);

if (cached) return cached;

const survey = await fetchSurvey(surveyId);
await cache.set(cacheKey, survey, 3600); // 1 hour TTL

Cache Keys

Always use createCacheKey.* utilities:
import { createCacheKey } from "@/lib/cache";

const surveyKey = createCacheKey.survey(surveyId);
const responseKey = createCacheKey.response(responseId);
Do not use Next.js unstable_cache(). Use React cache() or explicit Redis caching.

Internationalization

Using Translations

All user-facing text must use t() function:
import { useTranslation } from "react-i18next";

const Component = () => {
  const { t } = useTranslation();
  return <h1>{t("common.welcome")}</h1>;
};

Translation Keys

Use lowercase with dots for nesting:
{
  "common": {
    "welcome": "Welcome",
    "save": "Save",
    "cancel": "Cancel"
  },
  "survey": {
    "create": "Create Survey",
    "delete": "Delete Survey"
  }
}

Comments and Documentation

JSDoc Comments

Add JSDoc for complex functions:
/**
 * Calculates the average response time for a survey
 * 
 * @param surveyId - The survey ID
 * @param startDate - Optional start date for filtering
 * @returns Average response time in seconds
 */
export const calculateAverageResponseTime = async (
  surveyId: string,
  startDate?: Date
): Promise<number> => {
  // Implementation
};

Inline Comments

Add comments for non-obvious code:
// Calculate cache TTL based on survey status
// Active surveys: 5 min, Draft: 1 hour, Completed: 24 hours
const ttl = survey.status === "active" ? 300 : 
            survey.status === "draft" ? 3600 : 86400;

TODO Comments

Use TODO for future improvements:
// TODO: Add pagination support
// TODO: Optimize query performance
// TODO(username): Handle edge case for archived surveys

Best Practices

Keep Functions Small

Each function should do one thing well:
const validateSurvey = (survey: Survey) => { ... };
const saveSurvey = (survey: Survey) => { ... };
const notifyUsers = (surveyId: string) => { ... };

const createSurvey = async (data: unknown) => {
  const survey = validateSurvey(data);
  await saveSurvey(survey);
  await notifyUsers(survey.id);
};

Remove Dead Code

Delete unused code instead of commenting it out:
// Code removed - use git history if needed
const activeFn = () => { ... };

Avoid Deep Nesting

Use early returns to reduce nesting:
const processSurvey = (survey: Survey | null) => {
  if (!survey) return null;
  if (!survey.isActive) return null;
  if (survey.responses.length === 0) return null;
  
  return analyzeSurvey(survey);
};

Use Const by Default

Prefer const over let:
const surveys = await getSurveys(); // Immutable reference
let count = 0; // Only when mutation is needed

Destructure Objects

Use destructuring for cleaner code:
const { name, status, questions } = survey;

Testing Considerations

Write testable code:
  • Pure functions: Easier to test
  • Dependency injection: Mock dependencies
  • Single responsibility: Test one thing
// Testable: Pure function with no side effects
const calculateScore = (responses: Response[]): number => {
  return responses.reduce((sum, r) => sum + r.score, 0);
};

// Also testable: Clear dependencies
const createSurvey = async (
  data: CreateSurveyInput,
  db: PrismaClient = prisma
) => {
  return await db.survey.create({ data });
};
See Testing Standards for more details.

SonarQube

We use SonarQube to identify code smells and security hotspots. Address SonarQube findings before submitting PRs.

Pre-commit Hooks

We use Husky and lint-staged for pre-commit checks:
{
  "lint-staged": {
    "*.{ts,tsx,js,jsx}": ["prettier --write"],
    "*.json": ["prettier --write"],
    "schema.prisma": ["prisma format"]
  }
}
These run automatically on commit.

Resources

Testing Standards

Learn how to write tests

Documentation Standards

Write great documentation

Contributing Guide

Contribution guidelines

Architecture

Understand the architecture