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
| Type | Convention | Example |
|---|---|---|
| Files | kebab-case | sleep-api.ts |
| Classes | PascalCase | WhoopClient |
| Functions/Variables | camelCase | getSleepData |
| Constants | SCREAMING_SNAKE | MAX_RETRY_COUNT |
| Types/Interfaces | PascalCase | SleepData |
| Enums | PascalCase | ScoreState |
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.tsnext 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