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
anyin new code. - If a library returns
any, define a proper interface/type and cast withas MyType. - Prefer
unknown+ runtime type narrowing overanywhen 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
loggerutility fromsrc/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: implementorthrow 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()ordebugPrint()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.mdfor available components likeBwScaffold,BwCard,BwPrimaryButton, etc. - Do not create one-off styled widgets when a shared component exists.
Radio Widget Pattern (Flutter 3.32+)
- Use
RadioGroupwrapper for all Radio and RadioListTile widgets. - As of Flutter 3.32,
groupValueandonChangedon individualRadio/RadioListTilewidgets are deprecated. - Wrap Radio widgets in a
RadioGroupthat 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:
RadioGrouptakesgroupValue(current selection) andonChanged(selection callback)- Individual
Radio/RadioListTilewidgets only needvalue - To disable a radio, use
enabled: falseon the individual widget (notonChanged: null)
Next.js / React
No any Type
- Same no-
anyrule as backend TypeScript. - Define proper interfaces for props, state, and API responses.
No Raw Console Logging
- Same no
console.log/console.errorrule 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
anyalways applies. - No console logging always applies.
- No emoji in UI always applies.
- Complete implementations always required.
- No
- When reviewing code or implementing features, check against this document first.
- These standards take precedence over convenience or speed.
References
- Backend logging utility:
src/lib/logger.ts - Design system: See Design System Documentation
- Architecture: See Architecture Overview