Skip to Content
DocsCoding conventionsCode organization

Code organization

  • Colocate related files in the same directory. Each component should have its own directory containing all its associated files. This includes:

    • Component file (e.g., patient-banner.component.tsx) - This is a React component.
    • Test file (e.g., patient-banner.test.tsx) - This is a unit or integration test file.
    • Stylesheet (e.g., patient-banner.scss) - This contains styles for the component.
    • Resource file (e.g., patient-banner.resource.ts) - This is a file that contains data fetching logic.
    • Any other related files (e.g., constants, types, utilities)

    Example directory structure:

    src/ └── patient-banner/ ├── patient-banner.component.tsx ├── patient-banner.test.tsx ├── patient-banner.scss └── patient-banner.resource.ts

    This approach:

    • Makes it easier to find and modify related files
    • Simplifies refactoring and maintenance
    • Keeps the codebase modular and well-organized
    • Makes it clear which files belong to which component
  • Avoid placing styles for multiple components in the same stylesheet. Instead, create a separate stylesheet for each component. This makes it easier to find the styles for a particular component.

  • Use @openmrs/create-o3-app through npm’s create initializer to quickly seed new O3 frontend modules:

    npm create @openmrs/o3-app@latest my-module -- --standalone

    The generator provides:

    • Standalone, existing monorepo, and new monorepo scaffolds
    • Rspack by default, with Webpack available through --webpack
    • Correct TypeScript configuration
    • routes.json, config-schema.ts, and lifecycle exports wired to @openmrs/esm-framework
    • Pre-configured testing setup with Vitest
    • ESLint and Prettier configurations
    • Husky and lint-staged setup
    • Basic project structure following O3 conventions
    • Starter page and extension component templates

    The template app  remains useful as a reference for the generated structure.

  • Group imports by source category, and keep imports within each group alphabetized. The recommended order is:

    • React and framework imports (e.g., React, useState, useEffect)
    • External modules (e.g., lodash, dayjs, react-i18next)
    • Carbon component imports (e.g., Button, InlineLoading)
    • OpenMRS imports (e.g., @openmrs/esm-framework, @openmrs/esm-patient-common-lib)
    • Local imports (components, hooks, utilities, etc.)
    • Asset imports (e.g. import styles from './user.scss')

    The current ESLint setup catches duplicate imports and type-only imports, but it does not enforce this full ordering automatically. Following this convention makes it easier to maintain consistency across the codebase.

  • Consolidate library imports into a single import statement. This makes it easier to see which modules are being used and makes the code more readable. For example, prefer:

    // Good import { Button, InlineLoading } from '@carbon/react';

    Over:

    // Bad import { Button } from '@carbon/react'; import { InlineLoading } from '@carbon/react';

    Note that you should still keep imports from different modules separate:

    // Good - separate imports for different modules import { useState, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; import { Button, DataTable } from '@carbon/react'; import { useConfig, showSnackbar } from '@openmrs/esm-framework';

    Type imports should be marked with the type keyword. An ESLint plugin will automatically flag any type imports that are not marked with the type keyword.

    // Good import { showModal, type Visit } from '@openmrs/esm-framework';
  • Place type annotations and interfaces at the top of the file, after the imports and above any component code. Since TypeScript types and interfaces are development-time constructs that get removed during compilation, they don’t affect the runtime behavior of your code.

    import { useTranslation } from 'react-i18next'; import { Button } from '@carbon/react'; import { showSnackbar } from '@openmrs/esm-framework'; // Type definitions come after imports, before component code interface UserData { id: string; name: string; email: string; role: 'admin' | 'user'; } type UserComponentProps = { userData: UserData; onSave: (data: UserData) => void; isEditable?: boolean; }; // Component code follows type definitions export function UserComponent({ userData, onSave, isEditable = false, }: UserComponentProps) { // ... component implementation }

    Some key points about type placement:

    • Keep related types and interfaces grouped together
    • Place more generic types before more specific ones that might depend on them
    • Consider extracting commonly used types into a separate types.ts file if they’re used across multiple components

    On whether to use type aliases or interface declarations:

    • Use interface when you need declaration merging or inheritance.
    • Use type for unions, intersections, primitives, tuples, and utility types.
    • Be consistent within your codebase - if your team has standardized on one approach, follow that convention.
    // Use interface for object shapes interface UserProps { name: string; age: number; onSave: (data: User) => void; }
    // Use type for unions and more complex types type Status = 'loading' | 'success' | 'error'; type ButtonKind = 'primary' | 'secondary' | 'ghost'; type Nullable<T> = T | null;
Last updated on