Introduction β
The goal of this document is to compile the development practices for the three main monorepos, aiming for more consistent and standardized development.
This document should remain straightforward and include only the universally agreed-upon elements. Other aspects will be addressed in retrospectives or during specific discussions.
During code reviews, developers must ensure adherence to established rules, making pragmatic exceptions as necessary. These rules will also be enforced by the linter where possible.
π΄: Rejected β To be reviewed in retrospective
π§: Candidate β Currently under discussion
π’: Approved by the Team β Consensus achieved
Development Guidelines β
1. Naming Conventions β
1.1 π’ Files and folders β
Files and folders names must be all lowercase and may include dashes (-).
π§In backend project, file names should directly correspond to their default export. For example, the task-service.ts file should have a default export like export default class TaskService {...}. Any additional named exports should be linked to the main export. source. Moreover the default export must not be a mere function.
eg: src/modules/domain/item-import-report/item-import-report.service.ts eg: src/components/features/user/user.tsx
Rationales
- Eliminates issues with non-case-sensitive file systems or configurations, such as those found in Linux/Windows, URLs, and similar environments.
- Enhances typing efficiency: eliminates the need to hold one key while pressing another.
- Improves readability: ensures clearer separation of words, enhancing overall clarity.
- Recommended by Google and VueJS
1.2 π’ File Extensions β
In certain cases, to better delineate the structure and functionality within our projects, we adopt specific file extensions. For instance, in NestJS projects, we use:
.service.ts.controller.ts.module.ts.dto.ts.test.ts.unit.ts
In frontend development, particularly with Redux, we employ
.slice.ts
Rationales
- Predominantly Utilized Convention in the Current Codebase
- Convention used in NestJS Official Documentation : example
1.3 π’ Class, Interface, Enum β
- Class, interface and enum are written in PascaleCase.
- No extension like .enum.ts or .interface.ts
- Interfaces should be named clearly and intuitively without including 'Interface' in the name. The use of 'Facade' as a suffix is permissible but not obligatory, reserved for cases where it clarifies the design pattern being applied.
- Enum keys are in UPPERCASE
eg: TaskService, TaskContext eg:
export enum TaskTimeoutPolicy {
RETRY = 'RETRY',
TIMEOUT = 'TIME_OUT_WF',
ALERT = 'ALERT_ONLY'
}
export enum TaskRetryLogic {
FIXED = 'FIXED',
EXPONENTIAL = 'EXPONENTIAL_BACKOFF'
}2
3
4
5
6
7
8
9
10
Rationales
- Predominantly Utilized Convention in the Current Codebase
- Book 'Clean Code' by Robert C Martin
1.4 π’ Environment variable β
- Environment variable names must be entirely UPPERCASE and may include underscores ( _ ).
- They should be clear and free of unnecessary words. eg:
# Avoid
environment:
- "main_maria_host=..."
# Recommended
environment:
- "MARIA_HOST=..."2
3
4
5
6
7
Rationales
- Predominantly Utilized Convention in the Current Codebase
- Guided by architectural decisions
1.5 π§Plural and Lists β
For filenames and variables that represent a collection or plural concept, use the 's' suffix instead of 'List' or the singular form, e.g. packages, tableIds.
Rationales
- Clean Code Robert C. Martin
1.6 π§Id β
IDs should be named either id or <myObject>Id.
eg.
// Avoid
function getTable(table) {...}
// Recommended
function getTable(id) {...}2
3
4
5
1.7 π’ Errors β
For enhanced clarity and to ensure the uniformity of error messaging across the application, the following convention should be used :
A. Formulation of Error Titles: β
- In English: Adopt the template
{Trigger Component} {Action} Errorfor structuring error messages.
Eg: "Table Navigation Error", "Table Edition Error", "Cell Action Error". - In French: Follow the template
Erreur d'{Action} de {DΓ©clencheur}for formulating error messages.
Eg: "Erreur de navigation vers la table", "Erreur d'Γ©dition de la table", "Erreur d'Γ©dition sur la cellule". - Ideally, translations are managed on the frontend side, and APIs should send raw data, including codes and descriptions.
B. Error Message Display: β
- Prefer using Ant Design's
notification.errorovermessage.errorfor the display of error messages to ensure a consistent user experience.
notification.error({
message: context.t('Item edition error'),
description: context.t('Attachment must be smaller than {limit} MB', { limit }),
});2
3
4
Rationales
- This approach is driven by the goal to standardize development practices and maintain consistency in user interface behavior as per architectural guidelines.
- Translations should be managed on the frontend, ensuring a clear separation of concerns, centralization of translation resources, and enabling dynamic localization based on the user's preferences or settings.
2. Folder and File Structure β
2.1 π’ Use LF endline β
Adopt LF (Line Feed) for line endings to ensure consistency across platforms and avoid build issues. Some tests file use CRLF to check parsing compatibility.
Rationales
- Ensures platform-independent consistency as part of our architectural standards.
- Prevents build failures in certain tools (worker) that do not recognize CRLF as valid line endings.
2.2 π’ Frontend: Feature-first project structure β
- Place specific components used within a single application in /src/features/[my-feature] for feature-specific components,
- Store common components used across multiple applications, like App, Settings, and Admin, in /lib/features/src/[my-feature] for feature-specific components, or /lib/components (* should be renamed in lib/common/) for shared react component. eg.
packages/
βββ app/src/
β βββ features/
β β βββ [my-feature]/
β β βββ ...
β βββ utils/
β βββ ...
βββ lib/
βββ features/
β βββ src/
β βββ auth/
β β βββ auth.tsx
β βββ ...
βββ common*/
βββ ...2
3
4
5
6
7
8
9
10
11
12
13
14
15
Rationales
- Guided by architectural decisions.
- Function vs Folder structure
2.3 π’ Backend Project Structure β
- Store service logic within the /services directory, aligning with NestJS's architectural recommendations.
- Utilize /utils directory and /domain for shared utilities or entities
Rationales
- Guided by architectural decisions.
- Function vs Folder structure
2.4 π’ Barrel files β
Barrel files, typically named index.ts, are used to export multiple components from a directory. In frontend projects, it's recommended to avoid using barrel files (see rationales).
// Not allowed in frontend
// services/index.ts
export * from './service1';
export * from './service2';
export * from './service3';2
3
4
5
6
Rationales
- When you import from a barrel file, you potentially end up importing the entire module or a large set of components/functions, even if you only need a small part of it. This practice can negate the benefits of lazy loading, as it prevents splitting your code into smaller chunks that can be loaded on demand. Thus, avoiding barrel files can be crucial for enabling effective lazy loading and subsequently optimizing the bundle size.
- Detailed explanation of the issues can be found in this Vercel article.
3. React usage β
3.1 π’ id, data-testid and className naming convention β
- Use HyphenCase (KebabCase) for id, data-testid and className eg:
<div className='h-100 d-flex flex-direction-column'>eg:<Input.TextArea placeholder='Enter description' data-testid="pipeline-form-input-textarea"/>eg:<div className='app'>eg:<div id="user-profile"></div>
Rationales
- Predominantly Utilized Convention in the Current Codebase
- Recommended by Google
3.2 π’ Component β
- Use PascalCase
- Define components as functions instead of classes
- Do not use React.FC unless it's necessary eg:
const Greeting = ({ name="" }) => {
return (<h1>Hello, {name}!</h1>);
}2
3
Rationales
- Recomended way to write component from Kent C Dodds article
- Component Class are deprecated : React official
- React.FC usage: The React TypeScript Cheatsheet and discussions like this Create React App pull request suggest it's often unnecessary and can complicate patterns in React 17 or earlier and TypeScript before 5.1. It may disrupt the Composition pattern, a core React concept, as noted here.
3.3 π’ Custom Hook β
- Use CamelCase for custom hook names.
- Custom hook names may include verbs to indicate actions, for example: useWakeup for a hook that triggers actions when the browser exits sleep mode.
- The filename for custom hooks should be prefixed with "use," for example: "use-wakeup.ts"
eg:
const useWakeup = () => {
...
}2
3
Rationales
- Guided by architectural decisions.
- Official React documentation
3.4 π’ Input and button (data-testid) β
- All Input and Button elements should include a data-testid attribute to facilitate End-to-End (E2E) testing.
- All Input and Button elements should be implemented as Antd components.
eg:
import { Form, Input } from 'antd';
...
<Form.Item {...FormLayouts.itemFormLayout} label='Description' name='description'>
<Input.TextArea placeholder='Enter description' data-testid="pipeline-form-input-textarea"/>
</Form.Item>2
3
4
5
6
7
Rationales
- Guided by architectural decisions.
- Essential for E2E testing using Playwright.
3.5 π’ Redux β
- Prefix Redux selectors with select, for example, selectAdminConfiguration.
- Redux slices should be named with a .slice.ts extension, for example: counter.slice.ts.
- Use the custom hook
useAsyncDispatchwhen you need to call an API that triggers an action or a modification in the Redux store.
- Example:
{
...
const asyncDispatch = useAsyncDispatch();
useEffect(() => {
const loadAccounts = async () => {
await asyncDispatch(getAccountsListAsyncAction());
};
loadAccounts();
}, []);
...
}
const getAccountsListAsyncAction = () =>
AsyncActionBuilder({
actions: {
load: accountsListSlice.actions.accountsListLoad,
success: accountsListSlice.actions.accountsListSuccess,
error: accountsListSlice.actions.accountsListFail
},
promise: async () => {
return getAllPages((page, pageSize) => {
return HakuClientFinder.LoggedClient().me().myAccounts(page, pageSize);
});
}
});2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
Rationales
- Establish uniform development standards
- Guided by architectural decisions.
3.6 π’ No Custom CSS β
To ensure consistency and maintainability in your codebase, adhere to the following principles:
- Prefer Ant Design Components: Use Ant Design for UI consistency and reduce custom CSS.
- Fallback to Tailwind Utilities: For specific styling needs not covered by Ant Design, use Tailwind's utility classes.
- Opt for
twin.macrostyled component for Complexity: When Tailwind classes clutter your markup, switch totwin.macrofor a tidier approach.
twin.macro example:
// Less ideal: Excessive classes
const Component = () => (
<div className="flex justify-center items-center p-4 bg-blue-500 hover:bg-blue-700">
<div className="px-4 bg-red-500">
{/* ... */}
</div>
<div className="px-4 mt-4 bg-blue-500">
{/* ... */}
</div>
</div>
)
// Recommended
const Component = () => (
<Cell>
<Header>
{/* ... */}
</Header>
<Description>
{/* ... */}
</Description>
</Cell>
);
const Cell = tw.div`flex justify-center items-center p-4 bg-blue-500 hover:bg-blue-700`;
const Header = tw.div`px-4 bg-red-500`;
const Description = tw.div`px-4 mt-4 bg-blue-500`;2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
Rationales
- Maintain a clean and readable codebase
- Ensures styling consistency across the development team.
- Avoids the accumulation of unused CSS, enhancing project performance.
3.7 π’ Translation Guidelines β
- Do not use backticks (``) for Strings in context.t() translations (see rationales). Always use single (' ') or double (" ") quotes for string identifiers.
- Use
useContext(I18nContext)for a more standard approach and easier mocking in Storybook instead ofgetTranslationContext().- Example:
const context = useContext(I18nContext);
context.t('You cannot edit this cell');2
Rationales
- The translation scripts (npm run trad-extract) do not recognize template literals so using them in
context.t()will not translate the string. However you can use them for punctuation. eg:context.t('Hello, I`m PL!')
4 Typescript usage β
4.1π’ Interface vs Type β
- Use interface for public API definitions, object shapes, and component props.
- Use type for specific values, union types, function types, and event types.
// Using interface for an object shape
export interface Bear {
name: string;
food: BearFood;
}
// Using type for union types
type BearFood = MammalFood | FishFood;2
3
4
5
6
7
8
Rationales
- Official typescript documentation: If you would like a heuristic, use interface until you need to use features from type.
- For more on why interfaces are preferred, see this video(MongoDB dev perspective)
4.2 π’ Avoid Solo Variables in Conditions β
Instead of using a variable alone (non boolean) in a condition (e.g., if (x) {...}), prefer explicit checks (e.g., if (x !== 0) {...}) for clarity and to prevent unintended coercion.
- If posible, use encapsulation condition from Robert C. Martin (Clean Code)
eg.
// Avoid
if (load) { β¦ }
// Recommended
if (load != null) { β¦ }
// Avoid
if (fsm.state === "fetching" && isEmpty(listNode)) { β¦ }
// Recommended
if (shouldShowSpinner(fsm, listNode)) { β¦ }2
3
4
5
6
7
8
9
10
11
12
Rationales
- Clean Code by Robert C. Martin
4.3 π’ Avoid Cluttering Your Code with Inferable Types β
- Avoid writing type annotations when TypeScript can infer the same type.
- Ideally your code has type annotations in function/method signatures but not on local variables in their bodies.
- Consider using explicit annotations for object literals and function return types even when they can be inferred. This will help prevent implementation errors from surfacing in user code.
// Avoid
const person: {
name: string;
born: {
where: string;
when: string;
};
died: {
where: string;
when: string;
}
} = {
name: 'Sojourner Truth',
born: {
where: 'Swartekill, NY',
when: 'c.1797',
},
died: {
where: 'Battle Creek, MI',
when: 'Nov. 26, 1883'
}
};
// Recommended
const person = {
name: 'Sojourner Truth',
born: {
where: 'Swartekill, NY',
when: 'c.1797',
},
died: {
where: 'Battle Creek, MI',
when: 'Nov. 26, 1883'
}
};
// Recommended
interface Product {
id: string;
name: string;
price: number;
}
function logProduct(product: Product) {
const {id, name, price} = product;
console.log(id, name, price);
}
// Avoid
function logProduct(product: Product) {
const {id, name, price}: {id: string; name: string; price: number } = product;
console.log(id, name, price);
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
Rationales
- From Effectivety Typescript book
- Frontend - Typescript Explicit vs Implict type: Theo video
4.4 π’ Test : Should... When β
- Adopt the 'Should... When' convention for naming test cases to clearly describe expected behavior and specific conditions.
- Additionally, use describe blocks to group tests, naming them after the service or component being tested for straightforward identification.
describe('UserService', () => {
it('should return user details when the user ID is valid', () => {
// Test implementation
});
});2
3
4
5
Rationales
- Guided by architectural decisions.
4.5 π’ Any vs Unknown β
If possible, avoid using any keywords (except if you work with javascript library); consider replacing them with 'unknown'.
// Recommended
export class RowDataCellEditRequest {
itemId: string;
fieldKey: string;
fieldVal: unknown;
suffix?: string;
withItem(itemId: string): RowDataCellEditRequest {
this.itemId = itemId;
return this;
}
withField(fieldKey: string, fieldValue: unknown): RowDataCellEditRequest {
this.fieldKey = fieldKey;
this.fieldVal = fieldValue;
return this;
}
withSuffix(suffix: string): RowDataCellEditRequest {
this.suffix = suffix;
return this;
}
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Rationales
5 Error Management β
π’ 5.1 Types of Exceptions in the Application β
- FrontError: Errors originating from the frontend application.
- PlError: Errors from Haku endpoints or other APIs.
- Error: General JavaScript errors not created by the developer.
Examples:
const frontError = new FrontError()
.withCode(FrontErrorCode.MISSING_STATE)
.withMessage('Current user local storage path oidc not set');
const plError = new PlError()
.withCode(PlErrorCode.UNAVAILABLE)
.withMessage(`Network Error during /whoami`);2
3
4
5
6
7
π’ 5.2 Exception Levels β
- Fatal: Unhandled generic errors generating an ErrorBoundary. These should be captured by Sentry and require investigation.
- Error: Serious errors preventing functionality, but handled and enriched by the developer (e.g., PlError, FrontError). These should be logged and notified.
- Warning: Undesirable errors mitigated in the code. These should be logged for later analysis but do not require immediate alerting.
Examples:
// Example with captureError()
const handleSubmit = async (values: FormCreateAccountValues) => {
setLoading(true);
try {
const plAccount = await new AccountService().createAccount(
{
companyName: values.companyName,
subDomain: values.subDomain,
zone: values.zone
},
values.email
);
navigate(`/accounts/${plAccount.id}`);
} catch (err) {
captureError(err);
notification.error({ message: `Cannot create account: ${err.message}` });
}
setLoading(false);
};
// Example with captureWarning()
const getPickerLocaleFromLanguage = (language: Language) => {
const found = datepickerLocaleMap.get(language);
if (found == null) {
captureWarning(
new FrontError()
.withMessage(`No datepicker locale found for language: ${language}`)
.withCode(FrontErrorCode.USER)
);
} else {
return found;
}
return datepickerLocaleMap.get(Language.eng) as PickerLocale;
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
π’ 5.3 Exception Handling β
- Error Handling: Display a message to the user, redirect, send to Sentry, etc.
- Re-throwing Errors: If not handled, the error should be re-thrown without processing, except for adding additional data (e.g., URL, application state).
- Anti-pattern to Avoid: Never handle and re-throw an error simultaneously. This complicates debugging and may hide underlying issues.
- API Calls: Ideally, API calls should be within a try/catch block.
Examples:
// Avoid
try {
/* Code that may throw error */
} catch(err) {
// Do not do both!
captureError(err);
throw err;
}
// Recommended
try {
/* Code that may throw error */
} catch(err) {
captureError(err);
}
// Recommended
try {
/* Code that may throw error */
} catch(err) {
throw err;
}
// Recommended
try {
/* Code that may throw error */
} catch(err) {
throw new FrontError().withCode(FrontErrorCode.UNKNOWN).withMessage(err.message);
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
π’ 5.4 Front Error Code β
- USER: User action error that does not affect the application beyond displaying a notification.
- UNKNOWN: Unidentified error.
- BAD_USAGE: Incorrect usage of a component not intended for that specific purpose.
- MISSING_PARAM: A required HTTP request parameter is missing.
- MISSING_STATE: A required React or Redux state is missing.
- NOT_FOUND: A required data was not found after an algorithmic search.
- ACCESS_FORBIDDEN: User does not have permission to access the component.
- ALREADY_EXISTS: Duplication is not allowed.
- BAD_FORMAT: Error during data conversion.
- INVALID_STATE: A required React or Redux state is present but incorrect.
- INVALID_PARAM: A required parameter is present but incorrect.
- NOT_INITIALIZED: Component or configuration is not initialized.
- MISSING_PROPS: A required prop for a React component is missing.
- NOT_IMPLEMENTED: Functionality is not implemented.