Code Style
This guide covers the coding standards for contributing to Discord Forum API.
Formatting
Prettier
All code is formatted with Prettier. Format is enforced on commit via pre-commit hooks.
Configuration (.prettierrc):
{ "semi": true, "singleQuote": true, "tabWidth": 2, "trailingComma": "es5", "printWidth": 100}Format code:
pnpm format # Format all filespnpm format:check # Check formattingEditorConfig
Ensure your editor respects .editorconfig:
root = true
[*]indent_style = spaceindent_size = 2end_of_line = lfcharset = utf-8trim_trailing_whitespace = trueinsert_final_newline = trueTypeScript
Strict Mode
TypeScript strict mode is enabled. This means:
strictNullChecks- No implicit null/undefinednoImplicitAny- Explicit types requiredstrictFunctionTypes- Strict function parameter checking
Type Imports
Use type-only imports when importing only types:
// Goodimport type { User, Thread } from './types';import { getThread } from './helpers';
// Badimport { User, Thread, getThread } from './types';Interfaces vs Types
Use interfaces for object shapes, types for unions/intersections:
// Interface for objectsinterface User { id: string; username: string;}
// Type for unionstype Status = 'open' | 'resolved' | 'locked';
// Type for complex typestype ThreadWithMessages = Thread & { messages: Message[] };Avoid any
Never use any. Use unknown for truly unknown types:
// Badfunction parse(data: any) { ... }
// Goodfunction parse(data: unknown) { if (typeof data === 'string') { // Now TypeScript knows it's a string }}Generic Constraints
Use constraints to ensure type safety:
// Goodfunction getProperty<T, K extends keyof T>(obj: T, key: K): T[K] { return obj[key];}
// Badfunction getProperty(obj: any, key: string) { return obj[key];}Naming Conventions
Variables & Functions
Use camelCase:
// Variablesconst threadCount = 10;const isActive = true;
// Functionsfunction getThreadById(id: string) { ... }async function fetchUserProfile() { ... }Types & Interfaces
Use PascalCase:
interface ThreadResponse { ... }type MessageStatus = 'sent' | 'pending';class ThreadService { ... }Constants
Use SCREAMING_SNAKE_CASE for true constants:
const MAX_THREADS_PER_PAGE = 100;const DEFAULT_CACHE_TTL = 3600;Files
Use kebab-case for files:
thread-service.tsuser-helpers.tsget-threads.test.tsDatabase
Use snake_case for database columns:
// Schemaexport const threads = sqliteTable('threads', { id: text('id').primaryKey(), serverId: text('server_id').notNull(), createdAt: integer('created_at').notNull(),});Code Organization
Imports Order
- External packages
- Internal packages (@discord-forum-api/*)
- Relative imports (../ then ./)
- Type imports last
// Externalimport { Hono } from 'hono';import { eq } from 'drizzle-orm';
// Internal packagesimport { db, threads } from '@discord-forum-api/db';
// Relativeimport { validateParams } from '../lib/validation';import { formatThread } from './helpers';
// Typesimport type { Context } from 'hono';import type { Thread } from '../types';Export Style
Prefer named exports over default exports:
// Goodexport function getThread() { ... }export const threadService = { ... };
// Avoidexport default function getThread() { ... }File Structure
// 1. Importsimport { ... } from '...';
// 2. Types/Interfaces (if not in separate file)interface Props { ... }
// 3. Constantsconst CACHE_TTL = 60;
// 4. Helper functions (private)function formatDate(date: Date) { ... }
// 5. Main exportsexport function getThreads() { ... }export function getThread(id: string) { ... }ESLint Rules
Key Rules
export default [ { rules: { // TypeScript '@typescript-eslint/no-explicit-any': 'error', '@typescript-eslint/consistent-type-imports': 'error', '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
// General 'no-console': ['warn', { allow: ['warn', 'error'] }], 'prefer-const': 'error', 'no-var': 'error', }, },];Disabling Rules
Only disable rules with justification:
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Discord API returns untyped dataconst rawData: any = await discordApi.fetch();Error Handling
Use Custom Errors
// Define custom errorsexport class NotFoundError extends Error { constructor(resource: string, id: string) { super(`${resource} not found: ${id}`); this.name = 'NotFoundError'; }}
export class ValidationError extends Error { constructor(message: string, public field?: string) { super(message); this.name = 'ValidationError'; }}
// Usageif (!thread) { throw new NotFoundError('Thread', threadId);}Async Error Handling
Always handle errors in async functions:
// Good - errors caught by frameworkapp.get('/threads/:id', async (c) => { const thread = await getThread(c.req.param('id')); if (!thread) { throw new NotFoundError('Thread', c.req.param('id')); } return c.json(thread);});
// Good - explicit try/catch when neededasync function syncThreads() { try { await fetchAllThreads(); } catch (error) { console.error('Sync failed:', error); // Handle gracefully }}Comments
When to Comment
Comment why, not what:
// Bad - describes what code does// Loop through threads and filterconst resolved = threads.filter(t => t.status === 'resolved');
// Good - explains why// Only show resolved threads on FAQ page to avoid confusionconst resolved = threads.filter(t => t.status === 'resolved');JSDoc for Public APIs
/** * Fetches threads from a server with pagination. * * @param serverId - Discord server ID * @param options - Pagination and filter options * @returns Paginated thread list * * @example * const { threads, nextCursor } = await getThreads('123456789', { * limit: 20, * status: 'open' * }); */export async function getThreads( serverId: string, options?: GetThreadsOptions): Promise<PaginatedThreads> { // ...}TODO Comments
Use TODO with context:
// TODO(username): Add pagination support when API v2 ships// TODO: Refactor this after Discord updates their APITesting Conventions
Test File Naming
threads.ts -> threads.test.tsuser-service.ts -> user-service.test.tsTest Structure
import { describe, it, expect, beforeEach, vi } from 'vitest';
describe('getThreads', () => { beforeEach(() => { // Reset state before each test });
describe('when server exists', () => { it('returns threads list', async () => { // Arrange const serverId = '123';
// Act const result = await getThreads(serverId);
// Assert expect(result.threads).toHaveLength(10); });
it('respects limit parameter', async () => { const result = await getThreads('123', { limit: 5 }); expect(result.threads).toHaveLength(5); }); });
describe('when server does not exist', () => { it('returns empty list', async () => { const result = await getThreads('nonexistent'); expect(result.threads).toHaveLength(0); }); });});Git Commit Style
Commit Message Format
<type>: <subject>
[optional body]Types
| Type | Description |
|---|---|
feat | New feature |
fix | Bug fix |
docs | Documentation |
style | Formatting (no code change) |
refactor | Code change that neither fixes nor adds |
test | Adding tests |
chore | Maintenance tasks |
Examples
feat: add leaderboard endpointfix: resolve thread sync race conditiondocs: update API reference for searchrefactor: extract message parser to shared libtest: add integration tests for threads APIchore: update dependencies