Skip to main content

Code Style Guide

This document establishes project-wide coding standards for all BookWish code. These rules ensure consistency, maintainability, and quality across the entire codebase.

TypeScript / Node Backend

No any Type

  • Never use any in new code.
  • If a library returns any, define a proper interface/type and cast with as MyType.
  • Prefer unknown + runtime type narrowing over any when the shape is truly dynamic.

Bad:

function processData(data: any) {
return data.name;
}

Good:

interface UserData {
name: string;
email: string;
}

function processData(data: UserData) {
return data.name;
}

// Or for truly dynamic data:
function processUnknown(data: unknown) {
if (typeof data === 'object' && data !== null && 'name' in data) {
return (data as { name: string }).name;
}
throw new Error('Invalid data shape');
}

No Raw Console Logging

  • No console.log, console.error, console.warn, or other raw console calls in committed code.
  • All logging must use the shared logger utility from src/lib/logger.ts.

Bad:

console.log('User created:', userId);
console.error('Payment failed:', error);

Good:

import { logger } from '../lib/logger';

logger.info('user.created', { userId });
logger.error('payment.failed', { error: error.message, userId });

No TODO Placeholders

  • No TODO comments or placeholder implementations in production paths.
  • Do not commit code with // TODO: implement or throw new Error("Not implemented") in routes, services, or controllers that are in active use.
  • If functionality is genuinely incomplete, either:
    • Complete it before committing
    • Return a proper error response with appropriate HTTP status code
    • Document the limitation in the work pack's acceptance criteria

Bad:

export async function deleteUser(userId: string) {
// TODO: implement user deletion
throw new Error('Not implemented');
}

Good:

export async function deleteUser(userId: string): Promise<void> {
await prisma.user.delete({ where: { id: userId } });
}

No Drive-By Refactors

  • Do not rename or restructure existing modules unless a work pack explicitly instructs you to.
  • Changes should be surgical and focused on the task at hand.
  • If you notice opportunities for improvement, document them for future work packs rather than changing them immediately.

Follow Existing Formatting

  • Do not change Prettier/ESLint config except where explicitly specified.
  • Follow existing code formatting conventions in the file you're editing.
  • Use the configured linter and formatter before committing.

Flutter / Dart

No Print Statements

  • No print() or debugPrint() calls in committed code.
  • Use proper logging mechanisms or remove debug statements before committing.

Bad:

void loadData() {
print('Loading data...');
debugPrint('User ID: $userId');
}

Good:

void loadData() {
// Use proper error handling and logging
// Production code should not output debug prints
}

No Emoji in UI Text

  • No emoji characters in user-facing UI text (labels, buttons, snackbars, dialog messages, etc.).
  • Emoji can render inconsistently across platforms and don't align with BookWish's professional, bookish aesthetic.

Bad:

Text('Welcome! 👋'),
ElevatedButton(
child: Text('Add to Wishlist ❤️'),
)

Good:

Text('Welcome'),
ElevatedButton(
child: Text('Add to Wishlist'),
)

SF-Style / Cupertino Icons

  • Icons must use SF-style / Cupertino icons (e.g., CupertinoIcons).
  • Avoid Material-specific icons unless there is no reasonable SF equivalent.
  • Never mix icon styles on the same screen.

Bad:

Icon(Icons.favorite) // Material icon
Icon(Icons.shopping_cart) // Mixed with Cupertino elsewhere

Good:

Icon(CupertinoIcons.heart)
Icon(CupertinoIcons.cart)

Composition Over Inheritance

  • Prefer composition over inheritance.
  • Keep widgets small and focused on a single responsibility.
  • Extract reusable pieces into separate widget functions or classes.

Bad:

class BookCard extends StatefulWidget {
// 300 lines of complex logic mixing UI and business logic
}

Good:

class BookCard extends StatelessWidget {
Widget build(BuildContext context) {
return BwCard(
child: Column(
children: [
BookCoverImage(url: coverUrl),
BookTitle(title: title),
BookAuthor(author: author),
],
),
);
}
}

Reuse Shared UI Components

  • Always use shared UI components from the design system instead of custom styling in each screen.
  • See docs/design-system.md for available components like BwScaffold, BwCard, BwPrimaryButton, etc.
  • Do not create one-off styled widgets when a shared component exists.

Radio Widget Pattern (Flutter 3.32+)

  • Use RadioGroup wrapper for all Radio and RadioListTile widgets.
  • As of Flutter 3.32, groupValue and onChanged on individual Radio / RadioListTile widgets are deprecated.
  • Wrap Radio widgets in a RadioGroup that manages the group state centrally.

Bad (deprecated):

Column(
children: [
RadioListTile<String>(
title: Text('Option A'),
value: 'a',
groupValue: _selectedValue, // deprecated
onChanged: (value) => setState(() => _selectedValue = value), // deprecated
),
RadioListTile<String>(
title: Text('Option B'),
value: 'b',
groupValue: _selectedValue,
onChanged: (value) => setState(() => _selectedValue = value),
),
],
)

Good:

RadioGroup<String>(
groupValue: _selectedValue,
onChanged: (value) {
if (value != null) setState(() => _selectedValue = value);
},
child: Column(
children: [
RadioListTile<String>(
title: Text('Option A'),
value: 'a',
),
RadioListTile<String>(
title: Text('Option B'),
value: 'b',
),
],
),
)

Key points:

  • RadioGroup takes groupValue (current selection) and onChanged (selection callback)
  • Individual Radio / RadioListTile widgets only need value
  • To disable a radio, use enabled: false on the individual widget (not onChanged: null)

Next.js / React

No any Type

  • Same no-any rule as backend TypeScript.
  • Define proper interfaces for props, state, and API responses.

No Raw Console Logging

  • Same no console.log / console.error rule as backend.
  • Follow the same error-handling and logging patterns as the backend where applicable.
  • Use structured logging for server-side Next.js code.

Follow Backend Patterns

  • Next.js API routes should follow the same service layer patterns, error handling, and validation as the main backend.
  • Reuse types from the backend where possible (via shared packages if applicable).

General Standards

Complete Implementations

  • Each work pack must fully implement all sub-tasks.
  • Partial implementations are not acceptable.
  • If a task cannot be completed, document why in the work pack response and get user approval before moving on.

Acceptance Criteria

  • When a work pack defines a checklist or acceptance criteria, all items must pass before the work is considered complete.
  • Run all specified validation commands (lint, build, test) and ensure they pass.
  • Verify that all files listed in the acceptance criteria exist and are correct.

Code Quality

  • Write self-documenting code with clear variable and function names.
  • Add comments only where the logic is non-obvious or requires context.
  • Prefer small, pure functions over large, stateful ones.
  • Handle errors explicitly; avoid silent failures.

Testing

  • When a work pack requires tests, write comprehensive tests covering:
    • Happy paths
    • Error cases
    • Edge cases
    • Integration between components
  • Ensure tests pass before considering the work complete.

How to Use This with Work Packs

  • Every future work pack assumes these standards.
  • If a work pack appears to conflict with these rules, the more restrictive rule wins.
    • No any always applies.
    • No console logging always applies.
    • No emoji in UI always applies.
    • Complete implementations always required.
  • When reviewing code or implementing features, check against this document first.
  • These standards take precedence over convenience or speed.

References