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.
Server Component (Default)
Client Component (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 );
};
}, []);
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 ,
},
});
Never use skip/offset with count():
const [ surveys , count ] = await Promise . all ([
prisma . survey . findMany ({
where: { environmentId },
take: 10 ,
}),
prisma . survey . count ({
where: { environmentId },
}),
]);
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"
}
}
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
};
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 ;
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