Code Guidelines
Comprehensive best practices and conventions for contributing to the Eddy codebase
Code Guidelines
Source: This page is based on docs/PRACTICE_AND_POLICY/CODE_GUIDELINES.md
This document outlines the best practices and conventions to follow when contributing to the codebase.
Last Updated: January 1, 2026
Codebase Structure
Our codebase is organized using a two-tier hierarchical structure:
Functional Domains (Top Level)
At the top level, we organize code by functional domain — grouping files by their technical purpose or layer in the architecture. Each functional domain represents a specific type of code or responsibility:
pages/- Next.js pages and API routescomponents/- React components and UI elementshooks/- Custom React hooks for data fetching and mutations, primarily usinguseQueryanduseMutationto interact with our API endpoints. These hooks encapsulate server state management and provide a consistent interface for components to fetch and mutate data.types/- TypeScript type definitionsutil/- Utility functions and helpersservices/- Business logic and service layerdb/- Database configuration and connectionmigrations/- Database schema migrationsserializers/- Data transformation and serialization logicabilities/- Authorization rules and permissionsstore/- State management (Zustand stores)constants/- Application-wide constantscontexts/- React context providersemail-templates/- Email templates
Business Domains (Second Level)
Within each functional domain, code is further organized by business domain or semantic domain — grouping related functionality by the core concepts and entities of our application.
For example, within pages/api/, you'll find business domains such as:
blocks/- Block-related endpointsworkflows/- Workflow-related endpointssessions/- Session-related endpointsusers/- User-related endpointsworkspaces/- Workspace-related endpoints
Similarly, within components/, you might find:
session/- Session-specific componentssheet/- Sheet-specific componentsbuilder/- Workflow builder componentsblocks/- Block components
This two-tier structure ensures that:
- Code is easy to find - Developers know to look in the functional domain first, then narrow down by business domain
- Related code stays together - All components for a business domain live in one place, all API endpoints for that domain live together, etc.
- Separation of concerns - Each functional layer has its own directory, making architectural boundaries clear
API Endpoints
Our API endpoints, located in /pages/api, follow a set of conventions to ensure consistency, predictability, and robustness.
File and Function Naming
We use a domain-based, action-oriented naming convention.
- File Structure: Endpoints are organized by domain, with each file representing a specific action. The structure is
pages/api/[domain]/[action].ts.- Example:
pages/api/blocks/create.ts - Example:
pages/api/block_options/delete.ts
- Example:
- Function Naming: The exported handler function should be named using
PascalCasein the formatDomainAction.- Example:
export default async function BlocksCreate(...) - Example:
export default async function BlockOptionsDelete(...)
- Example:
General Structure & Principles
- Async/Await: All API handlers are
asyncfunctions to handle asynchronous operations like database queries. - Try/Catch: The entire body of the handler function is wrapped in a
try...catchblock to handle unexpected errors gracefully. - RESTful: We strive for a RESTful approach. The file action (
create,update,delete,index) generally corresponds to HTTP verbs (POST, PUT/PATCH, DELETE, GET).
Here is a basic template for a new endpoint:
import { NextApiRequest, NextApiResponse } from 'next'
import knex from '../../../db'
import { YourResponseType } from '../../../types/yourType'
import { ApiErrorT } from '../../../types/error'
import getCatchErrors from '../../../util/api/getCatchErrors'
import { sendError } from '../../../util/api/sendError'
import validateUser from '../../../util/api/validateUser'
export default async function YourDomainAction(
req: NextApiRequest,
res: NextApiResponse<YourResponseType | ApiErrorT>
) {
// 1. Destructure required fields from the request body
const { field1, field2 } = req.body
try {
// 2. Authenticate the user
const user = await validateUser(req, res)
// 3. Validate required fields
if (!field1) {
return sendError(res, 'Missing required fields', 400)
}
// 4. (Optional) Authorization logic
// ...check if user has permission to perform this action
// 5. Perform business logic (e.g., database operations)
const result = await knex('your_table').where({ id: field1 })...
// 6. Send a success response
res.status(200).json(result)
} catch (err: any) {
// 7. Catch and handle any unexpected errors
getCatchErrors(res, err)
}
}Authentication & Authorization
- Authentication: User authentication is handled by the
validateUserutility. It should be the first call inside thetryblock. It validates the user's session and returns the user object. If authentication fails, it will automatically send an error response and halt execution.
const user = await validateUser(req, res)- Authorization: After a user is authenticated, you may need to perform additional checks to ensure they have permission to access or modify a specific resource. This logic should come after the
validateUsercall.
Typing Responses
- Success Type: The success response type should be explicitly defined in the function signature.
- Error Type: All endpoints should include
| ApiErrorTin their response type to account for our standardized error format.
// Good: Response can be a single BlockOptionT or an ApiErrorT
res: NextApiResponse<BlockOptionT | ApiErrorT>
// Good: Response can be an array of BlockOptionT or an ApiErrorT
res: NextApiResponse<BlockOptionT[] | ApiErrorT>Error Handling
We have two primary utility functions for sending error responses.
sendError(res, message, statusCode): Use this for expected errors where you have context, such as invalid input from the client.
if (!id || !label) {
return sendError(res, 'Missing required fields', 400)
}getCatchErrors(res, err): This is our global error handler for unexpected errors. It should be used exclusively in thecatchblock.
catch (err: any) {
getCatchErrors(res, err)
}Code Style & Best Practices
Functional Programming Principles
We favor functional programming paradigms throughout the codebase:
- Immutability: Avoid mutating data directly. Use spread operators,
map,filter, andreduceinstead of mutating arrays or objects.
// Good: Create new objects/arrays
const updatedUser = { ...user, name: 'New Name' }
const newItems = items.filter(item => item.active)
// Avoid: Mutating existing data
user.name = 'New Name'
items.splice(0, 1)-
Pure Functions: Functions should return the same output for the same input without side effects when possible.
-
Signal Side Effects: When side effects are necessary (API calls, database operations, DOM manipulation), make them explicit and isolated.
// Good: Side effect is clear from function name and placement
async function fetchUserData(userId: string) {
return await knex('users').where({ id: userId }).first()
}
// Good: Side effect isolated in a specific function
function updateDOMTitle(title: string): void {
document.title = title
}Function Naming
Functions should follow the convention {verb}{Domain} or {verb}{Domain}{Detail}:
- Use descriptive action verbs:
create,update,delete,fetch,get,set,validate,serialize,calculate,transform,format,handle - Include the domain: What entity or concept is being acted upon
- Be specific: The name should clearly communicate what the function does
// Good examples
function createWorkflow(data: WorkflowData) { ... }
function fetchUserSessions(userId: string) { ... }
function validateBlockInput(input: unknown) { ... }
function serializeWorkflowRun(run: WorkflowRun) { ... }
function calculateSessionDuration(start: Date, end: Date) { ... }
// Avoid: Vague or unclear names
function doStuff() { ... }
function handler() { ... }
function process(data: any) { ... }React Component Best Practices
Minimize useEffect Usage
useEffect can introduce complexity and bugs. Before using it, consider alternatives:
// Often unnecessary - derive state instead
// Avoid:
const [filteredItems, setFilteredItems] = useState([])
useEffect(() => {
setFilteredItems(items.filter(item => item.active))
}, [items])
// Good: Derive directly
const filteredItems = items.filter(item => item.active)When useEffect is necessary (side effects like API calls, subscriptions), ensure:
- Dependencies are correct and complete
- Cleanup functions are provided when needed
- The effect has a single, clear responsibility
Use useMemo Sparingly
Only use useMemo when you have measured performance issues:
- For expensive computations that run on every render
- When referential equality matters for child component re-renders
// Generally unnecessary - JavaScript is fast
// Avoid premature optimization:
const sum = useMemo(() => a + b, [a, b])
// Good use case: Expensive filtering/sorting of large datasets
const sortedAndFiltered = useMemo(
() => largeDataset.filter(filterFn).sort(sortFn),
[largeDataset, filterFn, sortFn]
)Keep Components Focused and Small
- Single Responsibility: Each component should have one clear purpose
- Size Guideline: If a component exceeds ~150-200 lines, consider breaking it down
- Extract Sub-Components: Isolate distinct UI sections into their own components
- Co-locate Components: Keep tightly coupled components together in the same file or subdirectory
// Instead of one massive component:
function LargeWorkflowBuilder() {
// 500 lines of mixed concerns...
}
// Break into focused components:
function WorkflowBuilder() {
return (
<>
<WorkflowHeader />
<WorkflowCanvas />
<WorkflowSidebar />
<WorkflowToolbar />
</>
)
}Component Structure
Organize component internals in a consistent order:
- Props interface/type definition
- Component declaration
- Hooks (useState, useEffect, custom hooks)
- Derived state and computed values
- Event handlers and callbacks
- Helper functions (consider extracting if complex)
- Render logic
interface WorkflowCardProps {
workflow: Workflow
onEdit: (id: string) => void
}
export default function WorkflowCard({ workflow, onEdit }: WorkflowCardProps) {
// Hooks
const [isHovered, setIsHovered] = useState(false)
const user = useCurrentUser()
// Derived values
const canEdit = checkWorkflowPermission(user, workflow, 'edit')
// Event handlers
const handleEdit = () => {
onEdit(workflow.id)
}
// Render
return (...)
}TypeScript Best Practices
- Explicit Types: Define types for function parameters and return values
- Avoid
any: Useunknownfor truly unknown types, then narrow with type guards - Type Inference: Let TypeScript infer types for simple variable assignments
- Interfaces vs Types: Use
interfacefor object shapes,typefor unions/intersections/utilities
// Good: Explicit parameter and return types
function calculateTotal(items: CartItem[]): number {
return items.reduce((sum, item) => sum + item.price, 0)
}
// Avoid: Implicit any
function process(data) { ... }
// Good: Use unknown and narrow
function parseInput(input: unknown): ParsedData {
if (typeof input !== 'object' || input === null) {
throw new Error('Invalid input')
}
// Type guard narrows unknown to object
return input as ParsedData
}Import Organization
Group and order imports logically for readability:
- External dependencies (React, Next.js, libraries)
- Internal modules (utils, types, services)
- Components
- Styles
// External
import { useState } from 'react'
import knex from 'knex'
import { NextApiRequest, NextApiResponse } from 'next'
// Internal utilities and types
import { ApiErrorT } from '../../../types/error'
import { WorkflowT } from '../../../types/workflow'
import { validateUser } from '../../../util/api/validateUser'
// Components
import { Button } from '../../components/basic/Button'
import { Modal } from '../../components/basic/Modal'
// Styles
import styles from './WorkflowCard.module.css'Naming Conventions
- Variables & Functions:
camelCase - Components & Types:
PascalCase - Constants:
UPPER_SNAKE_CASEfor true constants - Files: Match the primary export (e.g.,
WorkflowCard.tsxexportsWorkflowCard) - Be Descriptive: Favor clarity over brevity
// Good
const userId = user.id
const MAX_RETRY_ATTEMPTS = 3
function fetchWorkflowData() { ... }
type UserSession = { ... }
interface WorkflowBuilderProps { ... }
// Avoid single-letter or cryptic names (except loop indices)
const x = user.id
const temp = data.filter(...)Comments & Documentation
- Code Should Be Self-Documenting: Write clear code that doesn't need comments
- Comment the "Why", Not the "What": Explain reasoning, not obvious operations
- Document Complex Logic: Add explanations for non-obvious algorithms or business rules
- JSDoc for Public APIs: Use JSDoc for functions meant to be used across the codebase
// Avoid: Stating the obvious
// Increment counter by 1
counter++
// Good: Explaining non-obvious reasoning
// Use a 30-second delay to debounce rapid user actions
// and prevent overwhelming the API with requests
const DEBOUNCE_DELAY = 30000
// Good: Documenting complex business logic
/**
* Validates that a user can transition a workflow stage.
*
* Rules:
* - User must be assigned to the current stage
* - All required blocks in the stage must be completed
* - Stage must not be locked by another user
*/
function validateStageTransition(user: User, stage: Stage): boolean {
// ...
}Error Handling
- Be Specific: Provide clear, actionable error messages
- Fail Fast: Validate inputs early and return errors immediately
- Handle Errors Appropriately: Use try-catch for unexpected errors, conditional checks for expected ones
- User-Facing Errors: Make error messages understandable for end users
// Good: Clear validation with specific error
if (!workflow.name || workflow.name.trim().length === 0) {
return sendError(res, 'Workflow name is required', 400)
}
// Good: Specific, actionable error message
if (session.status === 'completed') {
return sendError(res, 'Cannot modify a completed session', 400)
}