Skip to content

Documentation (JSDoc)

Use JSDoc comments for public APIs to improve IDE support and generate documentation.

Comment Style

A comment states only the constraint the code cannot show. Keep it to 1-3 lines.

Never write:

  • History or dates ("changed in June", "was previously async")
  • Decision narration ("we tried X, then chose Y")
  • A restatement of what the next line already says
typescript
// ✅ GOOD - states a constraint the code cannot show
// winston is an optional peer; a compiled binary must register a provider explicitly,
// because only a class reference carries one into the bundle.
LoggerFactory.use({ provider: PinoLogger });

// ❌ BAD - restates the code, narrates history
// 2026-07-18: after discussing with the team we decided to switch the logger
// provider. This line calls use() on LoggerFactory and passes PinoLogger.
LoggerFactory.use({ provider: PinoLogger });

When to Use JSDoc

ContextRequired?Reason
Public methodsYesConsumers need documentation
Exported functionsYesAPI contract documentation
Complex typesYesClarify usage
Private methodsNoInternal, can change
Self-explanatory codeNoAvoid redundant docs

JSDoc Format

typescript
/**
 * Brief description of what the function does.
 *
 * @param opts - Description of the options object
 * @param opts.filter - Query filter with where, limit, offset
 * @param opts.options - Additional options like transaction
 * @returns Promise resolving to the query result
 *
 * @example
 * const users = await userRepository.find({
 *   filter: { where: { status: 'ACTIVE' }, limit: 10 },
 * });
 *
 * @throws {ApplicationError} When validation fails
 * @see {@link UserService} for business logic
 */
async find(opts: { filter: TFilter<TUser>; options?: IExtraOptions }): Promise<TUser[]> {
  // implementation
}

Common JSDoc Tags

TagUsage
@paramDocument function parameters
@returnsDocument return value
@throwsDocument thrown exceptions
@exampleProvide usage examples
@seeReference related items
@deprecatedMark as deprecated with migration path
@internalMark as internal (not public API)
@sinceVersion when feature was added
@defaultDefault value for optional parameter

Examples

Service Method

typescript
/**
 * Creates a new user account. Email must be unique; the password is hashed here, never by the caller.
 *
 * @param data - User creation data
 * @returns The created user without sensitive fields
 * @throws {ApplicationError} 409 if email already exists
 * @throws {ApplicationError} 422 if validation fails
 *
 * @example
 * const user = await userService.createUser({
 *   email: 'john@example.com',
 *   name: 'John Doe',
 *   password: 'SecurePass123!',
 * });
 */
async createUser(data: TCreateUserRequest): Promise<TUser> {
  // ...
}

Repository Method

typescript
/**
 * Finds entities matching the given filter.
 *
 * @param opts - Find options
 * @param opts.filter - Query filter
 * @param opts.filter.where - Conditions to match
 * @param opts.filter.limit - Maximum records to return
 * @param opts.filter.offset - Records to skip for pagination
 * @param opts.filter.order - Sort order (e.g., ['createdAt DESC'])
 * @param opts.filter.include - Relations to load
 * @returns Promise resolving to the array of matching entities
 *
 * @example
 * // Find active users, sorted by name
 * const users = await userRepository.find({
 *   filter: {
 *     where: { status: 'ACTIVE' },
 *     order: ['name ASC'],
 *     limit: 20,
 *   },
 * });
 */
async find<R = TUser>(opts: { filter: TFilter<TUser>; options?: IExtraOptions }): Promise<R[]> {
  // ...
}

Deprecation

typescript
/**
 * @deprecated Use {@link findById} instead. Will be removed in v2.0.
 *
 * @example
 * // Before (deprecated)
 * const user = await userRepository.getById('123');
 *
 * // After (recommended)
 * const user = await userRepository.findById({ id: '123' });
 */
async getById(id: string): Promise<TUser | null> {
  return this.findById({ id });
}

Type Documentation

typescript
/**
 * Options for configuring user audit columns.
 *
 * @property dataType - The database type for user IDs ('string' | 'number')
 * @property columnName - The column name in the database
 * @property allowAnonymous - Whether to allow null user IDs (default: true)
 *
 * @example
 * const opts: TUserAuditColumnOpts = {
 *   dataType: 'string',
 *   columnName: 'created_by',
 *   allowAnonymous: false,
 * };
 */
type TUserAuditColumnOpts = {
  dataType: 'string' | 'number';
  columnName: string;
  allowAnonymous?: boolean;
};

Interface Documentation

typescript
/**
 * Configuration for JWT authentication strategy.
 *
 * @interface IJWTStrategyOptions
 * @property secret - Secret key for signing tokens (required)
 * @property expiresIn - Token expiration time (default: '1h')
 * @property algorithm - Signing algorithm (default: 'HS256')
 * @property issuer - Token issuer claim
 * @property audience - Token audience claim
 */
interface IJWTStrategyOptions {
  secret: string;
  expiresIn?: string;
  algorithm?: 'HS256' | 'HS384' | 'HS512';
  issuer?: string;
  audience?: string;
}

Class Documentation

typescript
/**
 * Base repository providing CRUD operations for database entities.
 *
 * Extends this class to create entity-specific repositories with
 * type-safe operations and automatic schema binding.
 *
 * @template EntitySchema - The Drizzle table schema type
 *
 * @example
 * @repository({ model: User, dataSource: PostgresDataSource })
 * export class UserRepository extends DefaultRelationalRepository<typeof User.schema> {
 *   // Custom methods here
 * }
 *
 * @see {@link BaseRelationalEntity} for model definition
 * @see {@link BaseRelationalDataSource} for database connection
 */
class DefaultRelationalRepository<EntitySchema extends TTableSchemaWithId = TTableSchemaWithId> {
  // ...
}

Best Practices

Do

  • Write descriptions in third person ("Creates...", "Returns...", "Validates...")
  • Include @example for non-obvious usage
  • Document all parameters with @param
  • Use @throws for expected exceptions
  • Link to related items with @see and {@link}

Don't

  • Don't document obvious things (@param id - The ID - unhelpful)
  • Don't copy TypeScript types into JSDoc (they're already visible)
  • Don't write multi-paragraph descriptions for simple functions
  • Don't use JSDoc for private implementation details
  • Don't record history, dates, or the reasoning behind a past change

See Also