Skip to main content

Code Style Guide

Conventions and standards for Wellpipe code.

TypeScript

Strict Mode

All packages use TypeScript strict mode:

{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true,
"noImplicitReturns": true
}
}

Explicit Return Types

Public functions must have explicit return types:

// Good
export function getSleep(days: number): Promise<SleepData[]> {
return client.fetchSleep(days);
}

// Bad - missing return type
export function getSleep(days: number) {
return client.fetchSleep(days);
}

Prefer Interfaces for Object Shapes

// Good
interface UserConfig {
apiKey: string;
timeout?: number;
}

// Acceptable for unions/intersections
type Result = Success | Failure;

Naming Conventions

TypeConventionExample
Fileskebab-casesleep-api.ts
ClassesPascalCaseWhoopClient
Functions/VariablescamelCasegetSleepData
ConstantsSCREAMING_SNAKEMAX_RETRY_COUNT
Types/InterfacesPascalCaseSleepData
EnumsPascalCaseScoreState

File Organization

// 1. Imports - external first, then internal
import { z } from 'zod';
import { TokenProvider } from '@wellpipe/core';
import { formatDate } from '../utils';

// 2. Types/Interfaces
interface ClientConfig {
tokenProvider: TokenProvider;
}

// 3. Constants
const BASE_URL = 'https://api.whoop.com';

// 4. Main exports (classes, functions)
export class WhoopClient {
// ...
}

// 5. Helper functions (private)
function buildUrl(path: string): string {
return `${BASE_URL}${path}`;
}

Error Handling

Custom Error Classes

export class WhoopApiError extends Error {
constructor(
public readonly status: number,
public readonly body: string
) {
super(`WHOOP API error: ${status}`);
this.name = 'WhoopApiError';
}

isAuthError(): boolean {
return this.status === 401;
}

isRateLimited(): boolean {
return this.status === 429;
}
}

Throw on Unexpected Errors

// Good - throw and let caller handle
async function fetchData(): Promise<Data> {
const response = await fetch(url);
if (!response.ok) {
throw new ApiError(response.status);
}
return response.json();
}

// Bad - returning null hides errors
async function fetchData(): Promise<Data | null> {
try {
const response = await fetch(url);
return response.json();
} catch {
return null; // Caller doesn't know what went wrong
}
}

Async/Await

Always prefer async/await over .then():

// Good
async function getData(): Promise<Data> {
const response = await fetch(url);
const data = await response.json();
return data;
}

// Avoid
function getData(): Promise<Data> {
return fetch(url)
.then(response => response.json())
.then(data => data);
}

Zod Validation

Use Zod for runtime validation of external data:

import { z } from 'zod';

// Define schema
const SleepSchema = z.object({
id: z.string(),
start: z.string().datetime(),
score: z.number().min(0).max(100),
});

// Infer type
type Sleep = z.infer<typeof SleepSchema>;

// Parse and validate
const sleep = SleepSchema.parse(apiResponse);

Comments

JSDoc for Public APIs

/**
* Fetches sleep data for the specified date range.
*
* @param startDate - Start date in ISO 8601 format
* @param endDate - End date in ISO 8601 format
* @returns Array of sleep records
* @throws {WhoopApiError} If the API request fails
*/
export async function getSleep(
startDate: string,
endDate: string
): Promise<SleepRecord[]> {
// ...
}

Inline Comments for Complex Logic

// Convert milliseconds to hours, rounding to 1 decimal
const hours = Math.round(milliseconds / 360000) / 10;

// WHOOP returns strain on a logarithmic scale
// Values above 18 indicate significant overreaching
const isOverreaching = strain > 18;

Avoid Obvious Comments

// Bad
// Get the user
const user = getUser();

// Good (no comment needed)
const user = getUser();

Testing Conventions

  • Test files: *.test.ts next to source
  • Use descriptive test names
  • One assertion per test (when reasonable)
  • Mock external dependencies
describe('SleepApi', () => {
describe('getRecent', () => {
it('should return sleep records for the last 7 days', async () => {
// Arrange
const mockResponse = { records: [...] };
vi.mocked(fetch).mockResolvedValue(mockResponse);

// Act
const result = await sleepApi.getRecent(7);

// Assert
expect(result.records).toHaveLength(7);
});

it('should throw on API error', async () => {
vi.mocked(fetch).mockRejectedValue(new Error('Network error'));

await expect(sleepApi.getRecent(7)).rejects.toThrow('Network error');
});
});
});

Linting

We use ESLint with TypeScript rules. Run:

npm run lint      # Check
npm run lint:fix # Auto-fix

Key rules:

  • No unused variables
  • No explicit any
  • Consistent spacing
  • Import sorting