Code Style
This document outlines the code style conventions used in the Plus261 project. Following these conventions ensures code consistency and maintainability.
What You’ll Learn
Section intitulée « What You’ll Learn »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
Overview
Section intitulée « Overview »The project follows consistent style conventions to maintain code quality and readability across the codebase. All code should be:
- ✅ Formatted with oxfmt (see
.oxfmtrc.jsonat the repo root) - ✅ Linted with oxlint (see
.oxlintrc.jsonat the repo root) - ✅ Committed with Conventional Commits (enforced by commitlint)
- ✅ Type-safe with TypeScript
- ✅ Well-documented
- ✅ Following naming conventions
TypeScript
Section intitulée « TypeScript »Formatting
Section intitulée « Formatting »- oxfmt is used for automatic code formatting
- Configuration is in
.oxfmtrc.jsonat the repository root - Run
bun --bun run formatto 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.jsonat the repository root - Run
bun --bun run lintto check for issues. Usebun --bun run lint:fixto apply auto-fixes where supported
Type Safety
Section intitulée « Type Safety »Always use explicit types for function parameters and return values. Avoid any type - use unknown if type is truly unknown.
// ✅ Goodfunction getUserById(id: string): Promise<User | null> { // ...}
// ❌ Badfunction 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.
Naming Conventions
Section intitulée « Naming Conventions »Files and Directories
Section intitulée « Files and Directories »| 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/ |
Variables and Functions
Section intitulée « Variables and Functions »Use camelCase for variables and functions:
// ✅ Goodconst userId = '123';function getUserById(id: string) {}
// ❌ Badconst user_id = '123';function get_user_by_id(id: string) {}Constants
Section intitulée « Constants »Use UPPER_SNAKE_CASE for constants:
// ✅ Goodconst MAX_RETRY_COUNT = 3;const API_BASE_URL = 'https://api.example.com';
// ❌ Badconst maxRetryCount = 3;const apiBaseUrl = 'https://api.example.com';Classes and Types
Section intitulée « Classes and Types »Use PascalCase for classes, interfaces, and types:
// ✅ Goodclass UserService {}interface UserData {}type UserId = string;
// ❌ Badclass userService {}interface userData {}Use PascalCase for enum names and UPPER_SNAKE_CASE for enum values:
// ✅ Goodenum OrderStatus { PENDING = 'PENDING', CONFIRMED = 'CONFIRMED', DELIVERED = 'DELIVERED',}
// ❌ Badenum orderStatus { pending = 'pending', confirmed = 'confirmed',}Import Organization
Section intitulée « Import Organization »Import Order
Section intitulée « Import Order »Organize imports in this order:
- External libraries (Node.js, npm packages)
- Internal libraries (
@lib/...) - Relative imports (
./...,../...)
// 1. External librariesimport { ObjectType, ID } from '@lib/type-graphql';import { eq } from 'drizzle-orm';
// 2. Internal librariesimport { EnhancedField, IDField } from '@lib/type-graphql-query';import { db } from '@lib/db';
// 3. Absolute importsimport { Category } from '../category/dto/category.type';import { db } from '../database';
// 4. Relative importsimport { metadataColumns } from '../utils/metadata-columns';import { ProductInput } from './dto/product.input';Import Style
Section intitulée « Import Style »- Use named imports when possible
- Use default imports only when necessary
- Group related imports together
// ✅ Goodimport { ObjectType, ID, InputType } from '@lib/type-graphql';import { EnhancedField, IDField, Permission } from '@lib/type-graphql-query';
// ❌ Badimport * as TypeGraphQL from '@lib/type-graphql';import TypeGraphQLQuery from '@lib/type-graphql-query';Code Organization
Section intitulée « Code Organization »Module Structure
Section intitulée « Module Structure »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
Function Organization
Section intitulée « Function Organization »- Keep functions small and focused
- One function, one responsibility
- Extract complex logic into separate functions
// ✅ Goodasync function createProduct(input: ProductInput): Promise<Product> { validateProductInput(input); const product = await saveProduct(input); await notifyProductCreated(product); return product;}
// ❌ Badasync function createProduct(input: ProductInput): Promise<Product> { // 100 lines of mixed validation, saving, and notification logic}Class Organization
Section intitulée « Class Organization »Organize class members in this order:
- Static properties
- Instance properties
- Constructor
- Public methods
- 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 { // ... }}Comments and Documentation
Section intitulée « Comments and Documentation »JSDoc Comments
Section intitulée « JSDoc Comments »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> { // ...}Inline Comments
Section intitulée « Inline Comments »- 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 trailawait productService.softDelete(productId);
// ❌ Bad - explains what (obvious)// Delete the productawait productService.delete(productId);Error Handling
Section intitulée « Error Handling »Use Specific Error Types
Section intitulée « Use Specific Error Types »// ✅ Goodif (!user) { throw new NotFoundError('User not found');}
// ❌ Badif (!user) { throw new Error('User not found');}Async Error Handling
Section intitulée « Async Error Handling »Always handle errors in async functions:
// ✅ Goodtry { const user = await getUserById(id); return user;} catch (error) { logger.error('Failed to get user', { id, error }); throw new InternalServerError('Failed to retrieve user');}TypeScript Specific
Section intitulée « TypeScript Specific »Use Type Guards
Section intitulée « Use Type Guards »// ✅ Goodfunction isUser(obj: unknown): obj is User { return typeof obj === 'object' && obj !== null && 'id' in obj;}
// ❌ Badfunction isUser(obj: any): boolean { return obj && obj.id;}Prefer Interfaces for Object Shapes
Section intitulée « Prefer Interfaces for Object Shapes »// ✅ Goodinterface UserData { id: string; name: string;}
// ❌ Badtype UserData = { id: string; name: string;};Use const Assertions
Section intitulée « Use const Assertions »// ✅ Goodconst statuses = ['PENDING', 'CONFIRMED', 'DELIVERED'] as const;type Status = (typeof statuses)[number];
// ❌ Badconst statuses = ['PENDING', 'CONFIRMED', 'DELIVERED'];GraphQL Specific
Section intitulée « GraphQL Specific »Decorator Order
Section intitulée « Decorator Order »Follow this order for decorators:
For Output Types (@ObjectType):
- Class decorators (
@ObjectType,@QueryOptions,@ContextFilter) - Field decorators (
@EnhancedField,@IDField) - Relation decorators (
@Relation,@UnPagedRelation) - Authorization decorators (
@Authorize)
For Input Types (@InputType):
- Class decorator (
@InputType) - Field decorator (
@Fieldfromtype-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.
Svelte (Web Client)
Section intitulée « Svelte (Web Client) »Components
Section intitulée « Components »- 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>Commit messages
Section intitulée « Commit messages »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— notaddedoradds)
Commit types
Section intitulée « Commit types »| 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 |
Technical work without product impact
Section intitulée « Technical work without product impact »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; preferci: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).
Examples
Section intitulée « Examples »feat(product): add search by SKUfix(order): correct line total roundingdocs: document commit message conventionchore(deps): bump drizzle-ormci: add docker dind fallback for image buildsbuild(devops): auto-detect container builderBreaking changes
Section intitulée « Breaking changes »Mark breaking changes with ! in the header and a BREAKING CHANGE: footer. Commitlint rejects commits that use only one of the two.
# ✅ valid — both ! and footerfeat(api)!: remove legacy REST endpoints
BREAKING CHANGE: /v1/orders removed; use GraphQL mutations instead.
# ❌ rejected — ! without footerfeat(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)!:.
Git hooks (Lefthook)
Section intitulée « Git hooks (Lefthook) »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:
bunx lefthook installTest a message without committing:
echo "feat(test): example" | bunx commitlintBranch names
Section intitulée « Branch names »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 |
git checkout -b feat/add-product-searchgit push origin feat/add-product-searchQuick Reference
Section intitulée « Quick Reference »| 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 |
Next Steps
Section intitulée « Next Steps »Continue learning:
- 📚 Code Structure - Understanding codebase structure
- 🛠️ Development Guide - Development workflow
- 🧪 Testing - Testing guidelines
- 📖 Contributing Guidelines - Contribution process
Related Topics
Section intitulée « Related Topics »- Quick Start - Get started quickly
- Code Structure - Codebase organization
- Development Workflow - How to develop features
- Type GraphQL Query - Schema builder conventions