Skip to main content
LibreChat follows strict coding standards to maintain code quality, consistency, and performance across the codebase.

Project Structure

LibreChat is a monorepo with the following workspaces:
WorkspaceLanguageSidePurpose
/apiJS (legacy)BackendExpress server — minimize changes here
/packages/apiTypeScriptBackendNew backend code lives here (TS only)
/packages/data-schemasTypeScriptBackendDatabase models/schemas
/packages/data-providerTypeScriptSharedShared API types, endpoints, data-service
/clientTypeScript/ReactFrontendFrontend SPA
/packages/clientTypeScriptFrontendShared frontend utilities

Workspace Boundaries

All new backend code must be TypeScript in /packages/api.
  • Keep /api changes to the absolute minimum (thin JS wrappers calling into /packages/api)
  • Database-specific shared logic goes in /packages/data-schemas
  • Frontend/backend shared API logic goes in /packages/data-provider
  • Build data-provider from project root: npm run build:data-provider

Code Style Principles

Structure and Clarity

Never-nesting

Use early returns, flat code, minimal indentation. Break complex operations into well-named helpers.
// Good
if (!user) return null;
if (!user.isActive) return null;
return user.name;

// Bad
if (user) {
  if (user.isActive) {
    return user.name;
  }
}
return null;

Functional First

Prefer pure functions, immutable data, map/filter/reduce over imperative loops.Only use OOP when it clearly improves domain modeling or state encapsulation.

No Dynamic Imports

Avoid dynamic imports unless absolutely necessary.

DRY Principle

Don’t Repeat Yourself:
  • Extract repeated logic into utility functions
  • Use reusable hooks/higher-order components
  • Parameterize helpers instead of duplicating
  • Centralize constants and configuration

Iteration and Performance

Minimize looping — especially over shared data structures like message arrays, which are iterated frequently throughout the codebase.
Performance Guidelines:
  • Consolidate sequential O(n) operations into a single pass
  • Never loop over the same collection twice if work can be combined
  • Use Map/Set for lookups instead of Array.find/Array.includes
  • Avoid unnecessary object creation
  • Prevent memory leaks: careful with closures, dispose resources/event listeners, no circular references
Loop Preferences:
// Performance-critical or index-dependent
for (let i = 0; i < array.length; i++) {
  // ...
}

// Simple array iteration
for (const item of array) {
  // ...
}

// Object property enumeration only
for (const key in object) {
  // ...
}

Type Safety

Never use `any`

Explicit types for all parameters, return values, and variables.

Limit `unknown`

Avoid unknown, Record<string, unknown>, and as unknown as T assertions.A Record<string, unknown> almost always signals a missing explicit type definition.

Don't duplicate types

Before defining a new type, check if it exists in packages/data-provider.Reuse and extend existing types rather than creating redundant definitions.

Use proper types

  • Use union types, generics, and interfaces appropriately
  • All TypeScript and ESLint warnings/errors must be addressed
  • No unresolved diagnostics

Comments and Documentation

Philosophy: Write self-documenting code.
  • No inline comments narrating what code does
  • JSDoc only for complex/non-obvious logic or intellisense on public APIs
  • Single-line JSDoc for brief docs: /** Brief description */
  • Multi-line JSDoc for complex cases with parameters and return types
  • Avoid standalone // comments unless absolutely necessary
// Good - self-documenting
function getUserFullName(user: User): string {
  return `${user.firstName} ${user.lastName}`;
}

// Good - JSDoc for complex logic
/**
 * Merges conversation branches while preserving message order.
 * Uses depth-first traversal to maintain temporal consistency.
 */
function mergeBranches(branches: Branch[]): Message[] {
  // ...
}

// Bad - unnecessary comment
function add(a: number, b: number): number {
  // Add a and b together
  return a + b;
}

Import Order

Imports are organized into three sections:
1

Package imports

Sorted shortest to longest line length.
  • react is always first
  • Multi-line imports count total character length across all lines
import react from 'react';
import axios from 'axios';
import { useState, useEffect } from 'react';
2

Type imports

Sorted longest to shortest line length.
  • Package types first, then local types
  • Length sorting resets between sub-groups
  • Always use standalone import type { ... }
  • Never inline type inside value imports
import type { ReactNode, ComponentProps } from 'react';
import type { UserSettings } from '~/types';
import type { User } from './types';
3

Local/project imports

Sorted longest to shortest line length.
  • Multi-line imports count total character length
  • Imports with alias ~ treated same as relative imports
import { helperFunction } from '~/utils/helpers';
import { ComponentA } from './ComponentA';
import './styles.css';
ESLint automatically enforces these conventions with npm run lint --fix or pre-commit hooks.

Frontend-Specific Rules

Localization

All user-facing text must use useLocalize().
  • Only update English keys in client/src/locales/en/translation.json
  • Other languages are automated externally via Locize
  • Use semantic key prefixes: com_ui_, com_assistants_, etc.
import { useLocalize } from '~/hooks';

function MyComponent() {
  const localize = useLocalize();
  
  return <button>{localize('com_ui_submit')}</button>;
}

React Components

  • TypeScript for all React components with proper type imports
  • Semantic HTML with ARIA labels (role, aria-label) for accessibility
  • Group related components in feature directories (e.g., SidePanel/Memories/)
  • Use index files for clean exports
// Good component structure
import React from 'react';
import type { FC } from 'react';

interface ButtonProps {
  label: string;
  onClick: () => void;
  disabled?: boolean;
}

export const Button: FC<ButtonProps> = ({ label, onClick, disabled }) => (
  <button
    onClick={onClick}
    disabled={disabled}
    aria-label={label}
    role="button"
  >
    {label}
  </button>
);

Data Management

  • React Query (@tanstack/react-query) for all API interactions
  • Proper query invalidation on mutations
  • QueryKeys and MutationKeys in packages/data-provider/src/keys.ts
  • Feature hooks pattern: client/src/data-provider/[Feature]/queries.ts
Data-Provider Integration:
  • Endpoints: packages/data-provider/src/api-endpoints.ts
  • Data service: packages/data-provider/src/data-service.ts
  • Types: packages/data-provider/src/types/queries.ts
  • Use encodeURIComponent for dynamic URL parameters

Performance Optimization

  • Prioritize memory and speed efficiency at scale
  • Cursor pagination for large datasets
  • Proper dependency arrays to avoid unnecessary re-renders
  • Leverage React Query caching and background refetching

ESLint Configuration

The project uses a comprehensive ESLint configuration defined in eslint.config.mjs:

Key Rules

  • prettier/prettier: ‘error’ - Enforce Prettier formatting
  • no-nested-ternary: ‘warn’ - Avoid complex nested ternaries
  • no-unused-vars: Warns on unused variables (except those prefixed with _)
  • import/no-cycle: ‘error’ - Prevent circular dependencies
  • import/no-self-import: ‘error’ - Prevent self-imports
  • @typescript-eslint/no-explicit-any: ‘off’ (but avoid any in practice)
  • @typescript-eslint/no-unused-vars: Warns on unused variables
  • @typescript-eslint/no-unnecessary-condition: ‘off’
  • @typescript-eslint/strict-boolean-expressions: ‘off’
  • react/react-in-jsx-scope: ‘off’ - Not needed in modern React
  • react-hooks/rules-of-hooks: ‘error’ - Enforce hooks rules
  • react-hooks/exhaustive-deps: ‘warn’ - Check hook dependencies
  • react/prop-types: ‘off’ - Use TypeScript instead
  • i18next/no-literal-string: ‘error’ - Enforce localization for all user-facing text (jsx-text-only mode)

Running Linting

# Check for errors
npm run lint

# Auto-fix errors
npm run lint:fix

# Format code with Prettier
npm run format

Pre-commit Hooks

The project uses Husky and lint-staged to automatically lint and format code before commits: Configuration (.husky/lint-staged.config.js):
module.exports = {
  '*.{js,jsx,ts,tsx}': ['prettier --write', 'eslint --fix', 'eslint'],
  '*.json': ['prettier --write'],
};

TypeScript Conversion

LibreChat is transitioning from JavaScript to TypeScript:
1

Original State

Initially developed entirely in JavaScript
2

Frontend: Complete

Fully transitioned to TypeScript
3

Backend: In Progress

  • Legacy Express.js server remains in /api as JavaScript
  • All new backend code written in TypeScript under /packages/api
  • Shared database logic in /packages/data-schemas (TypeScript)
  • Shared API types/services in /packages/data-provider (TypeScript)
  • Minimize direct changes to /api; prefer adding TS code to /packages/api

Best Practices Summary

Do:
  • Write TypeScript for all new code
  • Use early returns and flat code structure
  • Prefer functional programming patterns
  • Consolidate loops and iterations
  • Use explicit types everywhere
  • Follow import ordering conventions
  • Localize all user-facing text
  • Write self-documenting code
  • Run linting before commits
Don’t:
  • Use any type
  • Create duplicate types
  • Loop over the same data multiple times
  • Use dynamic imports unnecessarily
  • Write inline comments explaining code
  • Add code directly to /api (use /packages/api instead)
  • Hardcode user-facing text (use i18n)
  • Leave ESLint warnings unresolved

Additional Resources

Build docs developers (and LLMs) love