Aller au contenu

Code Style

This document outlines the code style conventions used in the Plus261 project. Following these conventions ensures code consistency and maintainability.

This guide will help you understand:

  • ✅ TypeScript formatting and linting rules
  • ✅ Naming conventions for files, variables, classes
  • ✅ Import organization patterns
  • ✅ Code organization best practices
  • ✅ GraphQL and Svelte conventions

The project follows consistent style conventions to maintain code quality and readability across the codebase. All code should be:

  • ✅ Formatted with oxfmt (see .oxfmtrc.json at the repo root)
  • ✅ Linted with oxlint (see .oxlintrc.json at the repo root)
  • ✅ Committed with Conventional Commits (enforced by commitlint)
  • ✅ Type-safe with TypeScript
  • ✅ Well-documented
  • ✅ Following naming conventions
  • oxfmt is used for automatic code formatting
  • Configuration is in .oxfmtrc.json at the repository root
  • Run bun --bun run format to format code (from the root or from a package that defines the script)
  • oxlint is used for code linting (TypeScript/JavaScript)
  • Configuration is in .oxlintrc.json at the repository root
  • Run bun --bun run lint to check for issues. Use bun --bun run lint:fix to apply auto-fixes where supported

Always use explicit types for function parameters and return values. Avoid any type - use unknown if type is truly unknown.

// ✅ Good
function getUserById(id: string): Promise<User | null> {
// ...
}
// ❌ Bad
function getUserById(id: any): any {
// ...
}

Avoid any

Never use any type. Use unknown if the type is truly unknown, then use type guards to narrow the type.

Type Convention Example
Files kebab-case user-service.ts, product.module.ts
Directories kebab-case order-line/, product-image/
Module directories singular product/, order/, user/

Use camelCase for variables and functions:

// ✅ Good
const userId = '123';
function getUserById(id: string) {}
// ❌ Bad
const user_id = '123';
function get_user_by_id(id: string) {}

Use UPPER_SNAKE_CASE for constants:

// ✅ Good
const MAX_RETRY_COUNT = 3;
const API_BASE_URL = 'https://api.example.com';
// ❌ Bad
const maxRetryCount = 3;
const apiBaseUrl = 'https://api.example.com';

Use PascalCase for classes, interfaces, and types:

// ✅ Good
class UserService {}
interface UserData {}
type UserId = string;
// ❌ Bad
class userService {}
interface userData {}

Use PascalCase for enum names and UPPER_SNAKE_CASE for enum values:

// ✅ Good
enum OrderStatus {
PENDING = 'PENDING',
CONFIRMED = 'CONFIRMED',
DELIVERED = 'DELIVERED',
}
// ❌ Bad
enum orderStatus {
pending = 'pending',
confirmed = 'confirmed',
}

Organize imports in this order:

  1. External libraries (Node.js, npm packages)
  2. Internal libraries (@lib/...)
  3. Relative imports (./..., ../...)
// 1. External libraries
import { ObjectType, ID } from '@lib/type-graphql';
import { eq } from 'drizzle-orm';
// 2. Internal libraries
import { EnhancedField, IDField } from '@lib/type-graphql-query';
import { db } from '@lib/db';
// 3. Absolute imports
import { Category } from '../category/dto/category.type';
import { db } from '../database';
// 4. Relative imports
import { metadataColumns } from '../utils/metadata-columns';
import { ProductInput } from './dto/product.input';
  • Use named imports when possible
  • Use default imports only when necessary
  • Group related imports together
// ✅ Good
import { ObjectType, ID, InputType } from '@lib/type-graphql';
import { EnhancedField, IDField, Permission } from '@lib/type-graphql-query';
// ❌ Bad
import * as TypeGraphQL from '@lib/type-graphql';
import TypeGraphQLQuery from '@lib/type-graphql-query';

Follow the standard module structure:

module-name/
├── dto/
│ ├── module-name.type.ts
│ ├── module-name.input.ts
│ └── module-name-aggregate.type.ts
├── module-name.module.ts
├── module-name.resolver.ts (optional)
└── module-name.service.ts (optional)

Learn more: Code Structure Guide

  • Keep functions small and focused
  • One function, one responsibility
  • Extract complex logic into separate functions
// ✅ Good
async function createProduct(input: ProductInput): Promise<Product> {
validateProductInput(input);
const product = await saveProduct(input);
await notifyProductCreated(product);
return product;
}
// ❌ Bad
async function createProduct(input: ProductInput): Promise<Product> {
// 100 lines of mixed validation, saving, and notification logic
}

Organize class members in this order:

  1. Static properties
  2. Instance properties
  3. Constructor
  4. Public methods
  5. Private methods
class UserService {
// 1. Static properties
private static readonly MAX_RETRIES = 3;
// 2. Instance properties
private readonly db: Database;
// 3. Constructor
constructor(db: Database) {
this.db = db;
}
// 4. Public methods
async getUserById(id: string): Promise<User> {
// ...
}
// 5. Private methods
private validateUserId(id: string): void {
// ...
}
}

Use JSDoc for public functions and classes:

/**
* Retrieves a user by their unique identifier.
*
* @param id - The unique identifier of the user
* @returns The user object or null if not found
* @throws {NotFoundError} If the user does not exist
*/
async function getUserById(id: string): Promise<User | null> {
// ...
}
  • Use comments to explain why, not what
  • Avoid obvious comments
  • Use comments for complex logic or non-obvious decisions
// ✅ Good - explains why
// Using soft delete to maintain audit trail
await productService.softDelete(productId);
// ❌ Bad - explains what (obvious)
// Delete the product
await productService.delete(productId);
// ✅ Good
if (!user) {
throw new NotFoundError('User not found');
}
// ❌ Bad
if (!user) {
throw new Error('User not found');
}

Always handle errors in async functions:

// ✅ Good
try {
const user = await getUserById(id);
return user;
} catch (error) {
logger.error('Failed to get user', { id, error });
throw new InternalServerError('Failed to retrieve user');
}
// ✅ Good
function isUser(obj: unknown): obj is User {
return typeof obj === 'object' && obj !== null && 'id' in obj;
}
// ❌ Bad
function isUser(obj: any): boolean {
return obj && obj.id;
}
// ✅ Good
interface UserData {
id: string;
name: string;
}
// ❌ Bad
type UserData = {
id: string;
name: string;
};
// ✅ Good
const statuses = ['PENDING', 'CONFIRMED', 'DELIVERED'] as const;
type Status = (typeof statuses)[number];
// ❌ Bad
const statuses = ['PENDING', 'CONFIRMED', 'DELIVERED'];

Follow this order for decorators:

For Output Types (@ObjectType):

  1. Class decorators (@ObjectType, @QueryOptions, @ContextFilter)
  2. Field decorators (@EnhancedField, @IDField)
  3. Relation decorators (@Relation, @UnPagedRelation)
  4. Authorization decorators (@Authorize)

For Input Types (@InputType):

  1. Class decorator (@InputType)
  2. Field decorator (@Field from type-graphql - not @EnhancedField)
// ✅ Good - Output Type
@ObjectType()
@QueryOptions({ pagingStrategy: PagingStrategies.OFFSET })
@ContextFilter<Product>((ctx) => {
/* ... */
})
export class Product {
@IDField(() => ID)
id: string;
@EnhancedField(() => String)
name: string;
@Relation('category', () => Category)
category: Category;
}
// ✅ Good - Input Type
@InputType()
export class ProductInput {
@Field(() => String)
name: string;
@Field(() => String, { nullable: true })
description?: string;
}

Important

Never use @EnhancedField in @InputType classes. Always use @Field from type-graphql.

  • Use PascalCase for component names: UserCard.svelte, ProductList.svelte
  • One component per file
  • Keep components focused and reusable

Use typed props with Svelte 5 runes:

<script lang="ts">
interface Props {
userId: string;
name: string;
email?: string;
}
let { userId, name, email = '' }: Props = $props();
</script>

All commit messages must follow Conventional Commits. Validation runs via commitlint on every git commit (Lefthook commit-msg hook).

<type>(<scope>): <description>
[optional body]
[optional footer(s)]
  • type — what changed (feat, fix, docs, style, refactor, test, chore, build, ci, perf, revert)
  • scope — optional module or area (api, web, devops, …)
  • description — short imperative summary (no trailing period, ≤ 72 chars)
  • Use the imperative mood (add, fix, remove — not added or adds)
Type Use case Example
feat New feature feat(product): add search functionality
fix Bug fix fix(order): resolve price calculation
docs Documentation docs(api): update queries guide
style Code style style: format code with oxfmt
refactor Code refactoring refactor(user): simplify service
test Test additions test(product): add unit tests
chore Maintenance chore: update dependencies
build Build system build(web): update vite config
ci CI/CD ci: add lint job to pipeline
perf Performance perf(api): cache product queries
revert Revert commit revert: feat(product): add search

Use this section for tickets and commits that do not change platform behaviour for end users (UI, API contracts, business rules, runtime product flows).

Rule: if users or integrators would not notice the change, do not use feat or fix.

Situation Type Example
GitLab pipeline, runners, CI jobs ci ci: auto-detect nerdctl or docker image builder
Dockerfiles, image build scripts, dev tooling build build(devops): unify container build helpers
Dependency bumps, repo hygiene, tooling config chore chore(deps): bump drizzle-orm
Internal refactor, same behaviour refactor refactor(container): extract version-skew resolver
Docs only docs docs(ci): document container builder selection
Formatting / lint-only changes style style: apply oxfmt
Tests only (no production code change) test test(api): cover version endpoint

Release impact: only feat, fix, perf, and breaking changes bump the semver tag (scripts/lib/release-semver.ts). Technical types above produce no version bump when they are the only commits since the last tag.

When to use fix anyway:

  • fix(api): … — corrects a product bug visible to users or API consumers.
  • fix(ci): … — acceptable when the pipeline was broken and you want a patch release; prefer ci: if the change is preventive or tooling-only.

MR titles should follow the same convention as commit messages (e.g. ci: fallback docker for image builds, not feat: update CI).

Fenêtre de terminal
feat(product): add search by SKU
fix(order): correct line total rounding
docs: document commit message convention
chore(deps): bump drizzle-orm
ci: add docker dind fallback for image builds
build(devops): auto-detect container builder

Mark breaking changes with ! in the header and a BREAKING CHANGE: footer. Commitlint rejects commits that use only one of the two.

Fenêtre de terminal
# ✅ valid — both ! and footer
feat(api)!: remove legacy REST endpoints
BREAKING CHANGE: /v1/orders removed; use GraphQL mutations instead.
# ❌ rejected — ! without footer
feat(api)!: remove legacy REST endpoints
# ❌ rejected — footer without !
feat(api): remove legacy REST endpoints
BREAKING CHANGE: /v1/orders removed.

The ! goes after the type or scope, before the colon: feat!: or feat(api)!:.

Configured in lefthook.yml at the repository root:

Hook Runs on commit Purpose
pre-commit before commit format + lint:fix on the whole repo (parallel)
commit-msg message check commitlint — reject invalid messages

Commitlint config: commitlint.config.mjs (extends @commitlint/config-conventional, enforces breaking-change-exclamation-mark).

Hooks are installed automatically on bun install (prepare → lefthook install). Re-run manually if needed:

Fenêtre de terminal
bunx lefthook install

Test a message without committing:

Fenêtre de terminal
echo "feat(test): example" | bunx commitlint

Use the same prefixes as commit types: <type>/<short-kebab-description>.

Type Prefix Example
New feature feat/ feat/add-product-search
Bug fix fix/ fix/order-calculation-error
Documentation docs/ docs/update-api-guide
Refactoring refactor/ refactor/user-service
Tests test/ test/add-product-tests
Maintenance chore/ chore/update-dependencies
Build build/ build/web-vite-config
CI/CD ci/ ci/add-lint-job
Performance perf/ perf/cache-product-queries
Fenêtre de terminal
git checkout -b feat/add-product-search
git push origin feat/add-product-search
Convention Rule Example
Files kebab-case user-service.ts
Directories kebab-case order-line/
Variables camelCase userId
Functions camelCase getUserById
Constants UPPER_SNAKE_CASE MAX_RETRIES
Classes PascalCase UserService
Types PascalCase UserData
Enums PascalCase (name), UPPER_SNAKE_CASE (values) OrderStatus.PENDING

Continue learning:

  1. 📚 Code Structure - Understanding codebase structure
  2. 🛠️ Development Guide - Development workflow
  3. 🧪 Testing - Testing guidelines
  4. 📖 Contributing Guidelines - Contribution process