Skip to content

Code Style ​

Coding conventions and best practices for Go Bananas!.

TypeScript Guidelines ​

Strict Mode ​

TypeScript strict mode is enabled. All code must:

  • Have explicit types for function parameters
  • Handle null and undefined properly
  • Avoid any type
typescript
// ✅ Good - explicit types
function processImage(buffer: ArrayBuffer, options: ProcessOptions): Promise<ImageResult> {
  // Implementation
}

// ❌ Bad - implicit any
function processImage(buffer, options) {
  // Implementation
}

Type Definitions ​

Use interface for object shapes, type for unions/intersections:

typescript
// Objects - use interface
interface ImageMetadata {
  id: number;
  width: number;
  height: number;
  format: 'png' | 'jpeg' | 'webp';
}

// Unions - use type
type ImageFormat = 'png' | 'jpeg' | 'webp';
type Result<T> = { success: true; data: T } | { success: false; error: Error };

Null Handling ​

Always handle nullable values explicitly:

typescript
// ✅ Good - null coalescing
const limit = input.limit ?? 50;

// ✅ Good - optional chaining
const name = user?.profile?.displayName ?? 'Anonymous';

// ❌ Bad - implicit null check
const limit = input.limit || 50;  // Breaks for 0

// ❌ Bad - ignoring nullability
const name = user.profile.displayName;  // Might be null

Async/Await ​

Always use async/await over raw promises:

typescript
// ✅ Good
async function fetchData() {
  try {
    const response = await fetch(url);
    const data = await response.json();
    return data;
  } catch (error) {
    throw new Error('Fetch failed');
  }
}

// ❌ Bad - nested promises
function fetchData() {
  return fetch(url)
    .then(response => response.json())
    .then(data => data)
    .catch(error => { throw new Error('Fetch failed'); });
}

Naming Conventions ​

Files ​

kebab-case.ts
my-component.tsx
image-storage.ts

Variables and Functions ​

typescript
// camelCase for variables and functions
const imageBuffer = await fetchImage();
function processImageData() {}

// UPPER_SNAKE_CASE for constants
const MAX_IMAGE_SIZE = 20 * 1024 * 1024;
const DEFAULT_ASPECT_RATIO = 'square';

Classes and Interfaces ​

typescript
// PascalCase for classes and interfaces
class ImageStorageService {}
interface ImageMetadata {}
type ProcessingOptions = {};

Database Fields ​

typescript
// snake_case for database columns
const query = `
  SELECT
    tenant_id,
    created_at,
    image_count
  FROM tenants
`;

// camelCase when mapping to TypeScript
interface Tenant {
  tenantId: string;
  createdAt: Date;
  imageCount: number;
}

Error Handling ​

Custom Error Classes ​

typescript
// Define specific error types
export class ValidationError extends Error {
  constructor(message: string, public field: string) {
    super(message);
    this.name = 'ValidationError';
  }
}

export class AuthenticationError extends Error {
  constructor(message: string = 'Authentication required') {
    super(message);
    this.name = 'AuthenticationError';
  }
}

Error Pattern ​

typescript
async function processOperation() {
  const startTime = Date.now();

  try {
    // Main logic
    const result = await performAction();
    return result;
  } catch (error) {
    // Log for debugging
    console.error('Operation failed:', error);

    // Track failure
    await trackUsage({ operation: 'operation_failed' });

    // Rethrow with context
    throw new Error(
      `Operation failed: ${error instanceof Error ? error.message : 'Unknown error'}`
    );
  }
}

API Error Responses ​

typescript
function handleError(error: unknown): Response {
  if (error instanceof ValidationError) {
    return jsonResponse({
      success: false,
      error: { code: 'VALIDATION_ERROR', message: error.message }
    }, 400);
  }

  if (error instanceof AuthenticationError) {
    return jsonResponse({
      success: false,
      error: { code: 'AUTHENTICATION_ERROR', message: error.message }
    }, 401);
  }

  // Generic error - don't leak internals
  console.error('Unhandled error:', error);
  return jsonResponse({
    success: false,
    error: { code: 'INTERNAL_ERROR', message: 'An error occurred' }
  }, 500);
}

Zod Schemas ​

Schema Definition ​

typescript
import { z } from 'zod';

// Define schema with descriptions
export const CreateCharacterSchema = z.object({
  character_name: z
    .string()
    .min(1, 'Name is required')
    .max(100, 'Name too long')
    .describe('Character name'),

  base_prompt: z
    .string()
    .min(10, 'Prompt too short')
    .max(2000, 'Prompt too long')
    .describe('Character appearance description'),

  reference_image_ids: z
    .array(z.number().int().positive())
    .max(10)
    .optional()
    .describe('Reference image IDs'),
});

// Export inferred type
export type CreateCharacterInput = z.infer<typeof CreateCharacterSchema>;

Validation Pattern ​

typescript
async function handleRequest(params: unknown) {
  // Validate at entry point
  const input = CreateCharacterSchema.parse(params);

  // Use validated input
  return createCharacter(input);
}

Database Queries ​

Prepared Statements ​

Always use prepared statements:

typescript
// ✅ Good - prepared statement
const result = await db.prepare(
  'SELECT * FROM images WHERE tenant_id = ? AND id = ?'
).bind(tenantId, imageId).first();

// ❌ Bad - string interpolation (SQL injection risk)
const result = await db.prepare(
  `SELECT * FROM images WHERE tenant_id = '${tenantId}'`
).first();

Null Coalescing for Bindings ​

D1 doesn't accept undefined:

typescript
// ✅ Good
bindings.push(
  input.limit ?? 50,
  input.offset ?? 0,
  input.search ?? null
);

// ❌ Bad - undefined causes D1_TYPE_ERROR
bindings.push(input.limit, input.offset, input.search);

Query Building ​

typescript
function buildImageQuery(filters: ImageFilters) {
  const conditions: string[] = ['tenant_id = ?'];
  const bindings: (string | number | null)[] = [filters.tenantId];

  if (filters.search) {
    conditions.push('prompt LIKE ?');
    bindings.push(`%${filters.search}%`);
  }

  if (filters.operationType) {
    conditions.push('operation_type = ?');
    bindings.push(filters.operationType);
  }

  const query = `
    SELECT * FROM images
    WHERE ${conditions.join(' AND ')}
    ORDER BY created_at DESC
    LIMIT ? OFFSET ?
  `;

  bindings.push(filters.limit ?? 50, filters.offset ?? 0);

  return { query, bindings };
}

Comments ​

When to Comment ​

  • Complex algorithms
  • Non-obvious business logic
  • Workarounds for known issues
  • API documentation
typescript
// ✅ Good - explains why
// Using setTimeout to avoid rate limiting from Gemini API
await new Promise(resolve => setTimeout(resolve, 100));

// ✅ Good - documents public API
/**
 * Generate an image from a text prompt.
 * @param prompt - Text description of the image
 * @param options - Generation options
 * @returns Generated image metadata
 */
export async function generateImage(prompt: string, options: GenerateOptions) {}

// ❌ Bad - obvious from code
// Increment counter
counter++;

// ❌ Bad - outdated comment
// Returns user name (actually returns email now)
function getUser() { return user.email; }

JSDoc for APIs ​

typescript
/**
 * Create a new character for multi-scene generation.
 *
 * @param env - Cloudflare environment bindings
 * @param tenantId - Tenant identifier
 * @param input - Character creation input
 * @returns Created character record
 * @throws ValidationError if input is invalid
 * @throws DuplicateError if character name exists
 *
 * @example
 * ```typescript
 * const character = await createCharacter(env, 'acme-corp', {
 *   character_name: 'Luna',
 *   base_prompt: 'A young woman with red hair...'
 * });
 * ```
 */
export async function createCharacter(
  env: Env,
  tenantId: string,
  input: CreateCharacterInput
): Promise<Character> {
  // Implementation
}

File Organization ​

Imports ​

Order imports:

  1. Node built-ins
  2. External packages
  3. Internal modules
  4. Types
typescript
// 1. External
import { z } from 'zod';

// 2. Internal modules
import { trackUsage } from '../services/usage';
import { ImageStorageService } from '../services/image-storage';

// 3. Types
import type { Env, ImageMetadata } from '../types';

Exports ​

Prefer named exports:

typescript
// ✅ Good - named exports
export function createCharacter() {}
export const CreateCharacterSchema = z.object({});
export type CreateCharacterInput = z.infer<typeof CreateCharacterSchema>;

// Avoid - default exports (harder to refactor)
export default function createCharacter() {}

Testing Style ​

Test Structure ​

typescript
describe('createCharacter', () => {
  // Setup shared fixtures
  const mockEnv = createMockEnv();
  const tenantId = 'test-tenant';

  // Group related tests
  describe('validation', () => {
    it('rejects empty character name', () => {
      expect(() => CreateCharacterSchema.parse({ character_name: '' }))
        .toThrow('Name is required');
    });

    it('rejects name over 100 characters', () => {
      const longName = 'a'.repeat(101);
      expect(() => CreateCharacterSchema.parse({ character_name: longName }))
        .toThrow('Name too long');
    });
  });

  describe('success cases', () => {
    it('creates character with required fields', async () => {
      const result = await createCharacter(mockEnv, tenantId, {
        character_name: 'Luna',
        base_prompt: 'A young woman with red hair',
      });

      expect(result.character_name).toBe('Luna');
      expect(result.id).toBeDefined();
    });
  });
});

Next Steps ​

Released under the MIT License.