This guide covers both enforceable rules and subjective “taste” elements that make the Documenso codebase consistent and maintainable.
General Principles
Follow these core principles when writing code for Documenso:
Functional over Object-Oriented : Prefer functional programming patterns over classes
Explicit over Implicit : Be explicit about types, return values, and error cases
Early Returns : Use guard clauses and early returns to reduce nesting
Immutability : Favor const over let; avoid mutation where possible
TypeScript Conventions
Type Definitions
Prefer type over interface
Avoid interfaces
// ✅ Prefer `type` over `interface`
type CreateDocumentOptions = {
templateId : number ;
userId : number ;
recipients : Recipient [];
};
Type Imports
Use the type keyword for type-only imports:
// ✅ Use `type` keyword for type-only imports
import type { Document , Recipient } from '@prisma/client' ;
import { DocumentStatus } from '@prisma/client' ;
// Types in function signatures
export const findDocuments = async ({ userId , teamId } : FindDocumentsOptions ) => {
// ...
};
// ✅ Extract inline types to named types
type FinalRecipient = Pick < Recipient , 'name' | 'email' | 'role' | 'authOptions' > & {
templateRecipientId : number ;
fields : Field [];
};
const finalRecipients : FinalRecipient [] = [];
Import Organization
Imports should be organized in the following order with blank lines between groups:
// 1. React imports
import { useCallback , useEffect , useMemo } from 'react' ;
// 2. Third-party library imports (alphabetically)
import { zodResolver } from '@hookform/resolvers/zod' ;
import { Trans } from '@lingui/react/macro' ;
import type { Document , Recipient } from '@prisma/client' ;
import { DocumentStatus , RecipientRole } from '@prisma/client' ;
import { match } from 'ts-pattern' ;
// 3. Internal package imports (from @documenso/*)
import { AppError } from '@documenso/lib/errors/app-error' ;
import { prisma } from '@documenso/prisma' ;
import { Button } from '@documenso/ui/primitives/button' ;
// 4. Relative imports
import { getTeamById } from '../team/get-team' ;
import type { FindResultResponse } from './types' ;
Functions & Methods
Arrow Functions
Always use arrow functions for consistency:
// ✅ Always use arrow functions
export const createDocument = async ({
userId ,
title ,
} : CreateDocumentOptions ) => {
// ...
};
// ✅ Callbacks and handlers
const onSubmit = useCallback ( async () => {
// ...
}, [ dependencies ]);
Function Parameters
Use destructured object parameters for multiple params:
// ✅ Use destructured object parameters
export const findDocuments = async ({
userId ,
teamId ,
status = ExtendedDocumentStatus . ALL ,
page = 1 ,
perPage = 10 ,
} : FindDocumentsOptions ) => {
// ...
};
React Components
Component Definition
// ✅ Use const with arrow function
export const AddSignersFormPartial = ({
documentFlow ,
recipients ,
fields ,
onSubmit ,
} : AddSignersFormProps ) => {
// ...
};
Hooks
Group related hooks together with blank line separation:
const { _ } = useLingui ();
const { toast } = useToast ();
const { currentStep , totalSteps , previousStep } = useStep ();
const form = useForm < TFormSchema >({
resolver: zodResolver ( ZFormSchema ),
defaultValues: {
// ...
},
});
Event Handlers
// ✅ Use arrow functions with descriptive names
const onFormSubmit = async () => {
await form . trigger ();
// ...
};
const onFieldCopy = useCallback (
( event ?: KeyboardEvent | null ) => {
event ?. preventDefault ();
// ...
},
[ dependencies ],
);
// ✅ Inline handlers for simple operations
< Button onClick = {() => setOpen ( false )} > Close </ Button >
State Management
// ✅ Descriptive state names with auxiliary verbs
const [ isLoading , setIsLoading ] = useState ( false );
const [ hasError , setHasError ] = useState ( false );
const [ showAdvancedSettings , setShowAdvancedSettings ] = useState ( false );
// ✅ Complex state in single useState when related
const [ coords , setCoords ] = useState ({
x: 0 ,
y: 0 ,
});
Error Handling
Try-Catch Blocks
// ✅ Use try-catch for operations that might fail
try {
const document = await getDocumentById ({
documentId: Number ( documentId ),
userId: user . id ,
});
return {
status: 200 ,
body: document ,
};
} catch ( err ) {
return {
status: 404 ,
body: {
message: 'Document not found' ,
},
};
}
Throwing Errors
// ✅ Use AppError for application errors
throw new AppError ( AppErrorCode . NOT_FOUND , {
message: 'Template not found' ,
});
// ✅ Use descriptive error messages
if ( ! template ) {
throw new AppError ( AppErrorCode . NOT_FOUND , {
message: `Template with ID ${ templateId } not found` ,
});
}
Error Parsing on Frontend
// ✅ Parse errors on the frontend
try {
await updateOrganisation ({ organisationId , data });
} catch ( err ) {
const error = AppError . parseError ( err );
console . error ( error );
toast ({
title: t `An error occurred` ,
description: error . message ,
variant: 'destructive' ,
});
}
Naming Conventions
Variables
// ✅ camelCase for variables and functions
const documentId = 123 ;
const onSubmit = () => {};
// ✅ Descriptive names with auxiliary verbs for booleans
const isLoading = false ;
const hasError = false ;
const canEdit = true ;
const shouldRender = true ;
// ✅ Prefix with $ for DOM elements
const $page = document . querySelector ( '.page' );
const $inputRef = useRef < HTMLInputElement >( null );
Types and Schemas
// ✅ PascalCase for types
type CreateDocumentOptions = {
userId : number ;
};
// ✅ Prefix Zod schemas with Z
const ZCreateDocumentSchema = z . object ({
title: z . string (),
});
// ✅ Prefix type from Zod schema with T
type TCreateDocumentSchema = z . infer < typeof ZCreateDocumentSchema >;
Constants
// ✅ UPPER_SNAKE_CASE for true constants
const DEFAULT_DOCUMENT_DATE_FORMAT = 'dd/MM/yyyy' ;
const MAX_FILE_SIZE = 1024 * 1024 * 5 ;
// ✅ camelCase for const variables that aren't "constants"
const userId = await getUserId ();
Functions
// ✅ Verb-based names for functions
const createDocument = async () => {};
const findDocuments = async () => {};
const updateDocument = async () => {};
const deleteDocument = async () => {};
// ✅ On prefix for event handlers
const onSubmit = () => {};
const onClick = () => {};
const onFieldCopy = () => {};
Clarity Over Brevity
Prefer descriptive names
Avoid unclear abbreviations
// ✅ Prefer descriptive names over abbreviations
const recipientAuthenticationOptions = {};
const documentMetadata = {};
// ✅ Common abbreviations that are widely understood
const userId = 123 ;
const htmlElement = document . querySelector ( 'div' );
const apiResponse = await fetch ( '/api' );
Pattern Matching with ts-pattern
Use match from ts-pattern for complex conditionals:
import { match } from 'ts-pattern' ;
// ✅ Use match for complex conditionals
const result = match ( status )
. with ( ExtendedDocumentStatus . DRAFT , () => ({
status: 'draft' ,
}))
. with ( ExtendedDocumentStatus . PENDING , () => ({
status: 'pending' ,
}))
. with ( ExtendedDocumentStatus . COMPLETED , () => ({
status: 'completed' ,
}))
. exhaustive ();
// ✅ Use .otherwise() for default case when not exhaustive
const value = match ( type )
. with ( 'text' , () => 'Text field' )
. with ( 'number' , () => 'Number field' )
. otherwise (() => 'Unknown field' );
Database & Prisma
Query Structure
// ✅ Use select to limit returned fields
const user = await prisma . user . findFirst ({
where: { id: userId },
select: {
id: true ,
email: true ,
name: true ,
},
});
// ✅ Use include for relations
const document = await prisma . document . findFirst ({
where: { id: documentId },
include: {
recipients: true ,
fields: true ,
},
});
Transactions
// ✅ Use transactions for related operations
return await prisma . $transaction ( async ( tx ) => {
const document = await tx . document . create ({ data });
await tx . field . createMany ({ data: fieldsData });
await tx . documentAuditLog . create ({ data: auditData });
return document ;
});
Complex Where Clauses
// ✅ Build complex where clauses separately
const whereClause : Prisma . DocumentWhereInput = {
AND: [
{ userId: user . id },
{ deletedAt: null },
{ status: { in: [ DocumentStatus . DRAFT , DocumentStatus . PENDING ] } },
],
};
const documents = await prisma . document . findMany ({
where: whereClause ,
});
TRPC Patterns
Router Structure
// ✅ Destructure context and input at start
. query ( async ({ input , ctx }) => {
const { teamId } = ctx ;
const { templateId } = input ;
ctx . logger . info ({
input: { templateId },
});
return await getTemplateById ({
id: templateId ,
userId: ctx . user . id ,
teamId ,
});
});
Request/Response Schemas
// ✅ Name schemas clearly
const ZCreateDocumentRequestSchema = z . object ({
title: z . string (),
recipients: z . array ( ZRecipientSchema ),
});
const ZCreateDocumentResponseSchema = z . object ({
documentId: z . number (),
status: z . string (),
});
Each TRPC route should be in its own file: routers/teams/create-team.ts with an associated types file: routers/teams/create-team.types.ts
Documenso uses ESLint and Prettier to enforce code style:
# Lint all packages
npm run lint
# Auto-fix linting issues
npm run lint:fix
# Format code with Prettier
npm run format
ESLint Configuration
The project extends @documenso/eslint-config with these custom rules:
rules : {
'@next/next/no-img-element' : 'off' ,
'no-unreachable' : 'error' ,
'react-hooks/exhaustive-deps' : 'off' ,
}
When in Doubt
Consistency : Follow the patterns you see in similar files
Readability : Favor code that’s easy to read over clever one-liners
Explicitness : Be explicit rather than implicit
Whitespace : Use blank lines to separate logical sections
Early Returns : Use guard clauses to reduce nesting
Functional : Prefer functional patterns over imperative ones
View Full Code Style Guide See the complete code style guide on GitHub for more details and examples