@lineai/linkedin-api
Version:
Professional LinkedIn API client with TypeScript support, entity classes, and developer-friendly features. Perfect for AI coders, recruiting, lead generation, market research, and content analysis. Includes comprehensive JSDoc, helper constants (LOCATIONS
386 lines (315 loc) ⢠11.6 kB
Markdown
# API Data Implementation Guide
## Overview
This guide documents the process used to create the LinkedIn API package, serving as a blueprint for future API implementations. It captures the methodology, decisions, and patterns that lead to a developer-friendly npm package.
## š v1.3.1 Critical Fix
### Location Parameter Type Correction
- **Fixed LocationId Type**: Changed from `string & { __brand: 'LocationId' }` to `number & { __brand: 'LocationId' }`
- **Updated LOCATIONS Constants**: All values now correctly typed as numbers (e.g., `102277331` not `"102277331"`)
- **API Compatibility**: Eliminates "Payload is incorrect" errors when using location filters
- **Cross-Method Support**: Both profile search (GET with joined strings) and company search (POST with arrays) work correctly
```typescript
// Before v1.3.1 (BROKEN)
type LocationId = string & { __brand: 'LocationId' };
export const LOCATIONS = {
US: { SAN_FRANCISCO: "102277331" } // String caused API errors
};
// v1.3.1+ (FIXED)
type LocationId = number & { __brand: 'LocationId' };
export const LOCATIONS = {
US: { SAN_FRANCISCO: 102277331 } // Number works correctly
};
```
## v1.3.0 Technical Improvements
### Type Safety & Runtime Error Prevention
- **Added RapidAPI Response Types**: Complete TypeScript interfaces for all API responses
- **Eliminated shared callEndpoint**: Each method now makes direct, typed HTTP calls
- **Fixed data path bugs**: Prevented `response.data.data.items` vs `response.data.items` errors
- **TypeScript interfaces**: `RapidApiProfileSearchResponse`, `RapidApiCompanySearchResponse`, etc.
### Parameter Immutability Pattern
```javascript
// Before v1.3.0: Parameters were mutated
delete params.minItems; // Modified original object
// v1.3.0+: Defensive copying
const apiParams = { ...params };
delete apiParams.minItems; // Original params preserved
return new Result(data, this, params); // Store original
```
### Enhanced Caching Implementation
- **Universal caching**: All `get` methods now implement 15-minute cache
- **Cache keys**: Format `{method}:{param}` (e.g., `profile:satyanadella`)
- **Performance**: 100% cache hit reduction in response time
- **Automatic expiration**: Built-in 15-minute timeout
## Process Steps
### 1. Analyze Real API Data
**Critical first step: Always start with actual API responses**
- Collect real JSON responses for all endpoints (get, search, etc.)
- Store example responses in organized directories (e.g., `/profile`, `/company`, `/posts`)
- Compare schema documentation with actual data - **trust the data over docs**
- Identify patterns:
- Which fields can be null, empty strings, or missing entirely
- Differences between search results (limited data) vs full entity responses
- Nested data structures and their consistency
- Date/time formats used
### 2. Design User-Friendly Interfaces
**Goal: Hide complexity while maintaining flexibility**
#### Separate Search vs Full Data
- Search results return limited data (e.g., name, headline, URL)
- Full entities contain complete nested structures
- Create separate TypeScript interfaces for each
#### Entity Classes Pattern
```typescript
// Encapsulate complex data access
class LinkedInProfile {
constructor(private data: ProfileData) {}
// Simple getters with safe defaults
getFullName(): string {
return `${this.data.firstName || ''} ${this.data.lastName || ''}`.trim() || 'Unknown';
}
// Helper methods for computed values
getCurrentPosition(): Position | null {
return this.data.position?.[0] || null;
}
// Raw data escape hatch
get raw(): ProfileData { return this.data; }
}
```
#### Enable Seamless Data Loading
```typescript
// Search item can load full data
class ProfileSearchItem {
async loadFullProfile(): Promise<LinkedInProfile> {
return this.api.getProfile(this.username);
}
}
```
### 3. Implement Type Safety
**TypeScript as a first-class citizen**
#### Branded Types for IDs
```typescript
type LocationId = string & { __brand: 'LocationId' };
type CompanyId = string & { __brand: 'CompanyId' };
// Prevents mixing up different ID types
function searchByLocation(locationId: LocationId) { ... }
```
#### Comprehensive Type Definitions
- Export all interfaces for consumer use
- Create `.d.ts` file with complete type definitions
- Use JSDoc comments for all public methods
- Include examples in documentation
### 4. Add Developer Experience Enhancements
#### Helper Constants
```typescript
export const LOCATIONS = {
US: {
NEW_YORK: '105080838' as LocationId,
SAN_FRANCISCO: '102277331' as LocationId,
// ... more cities
}
};
// Usage: api.searchProfiles({ geo: [LOCATIONS.US.NEW_YORK] })
```
#### Smart Parameter Handling
```typescript
// Accept arrays and auto-join for API
searchProfiles(params: {
geo?: LocationId[] | string; // Accept array
}) {
if (Array.isArray(params.geo)) {
params.geo = params.geo.join(','); // API expects comma-separated
}
}
```
#### Consistent Data Access
- Always return Date objects for timestamps
- Always return arrays (never null/undefined)
- Provide sensible defaults for missing data
- Construct URLs when missing
### 5. Handle Common Pain Points
#### Rate Limiting
```typescript
// Expose rate limit info
interface ApiResponse<T> {
data: T;
rateLimit: {
remaining: number;
reset: Date;
};
}
// Typed errors for handling
class RateLimitError extends Error {
retryAfter: Date;
}
```
#### Pagination Helpers
```typescript
class SearchResults {
hasNextPage(): boolean { ... }
async getNextPage(): Promise<SearchResults> { ... }
}
```
#### Safe Defaults
- Return empty arrays instead of null/undefined
- Provide default avatar URLs for missing profile pictures
- Return 'Unknown' for missing names
- Return 0 for missing counts
#### Simple Caching
- Cache GET requests for 5 minutes
- Reduce unnecessary API calls
- Make it optional/configurable
### 6. Package Structure
```
linkedin-api/
āāā index.js # Main implementation
āāā index.d.ts # TypeScript definitions
āāā API_IMPLEMENTATION_GUIDE.md # This guide
āāā README.md # User documentation
āāā package.json # NPM configuration
āāā test-data/ # Real API responses
āāā profile/
āāā company/
āāā posts/
```
## Key Principles
### 1. **Start with Real Data**
Never trust documentation alone. Always work from actual API responses to understand the true data structure.
### 2. **Hide Complexity**
Entity classes should make complex nested data feel simple. Developers shouldn't need to know the API structure.
### 3. **Type Everything**
Full TypeScript support is non-negotiable. It prevents errors and provides excellent IDE support.
### 4. **Fail Gracefully**
- Safe defaults prevent runtime errors
- Clear, typed error messages
- Never throw on missing optional data
### 5. **Developer First**
Every decision should optimize for the consuming developer's experience:
- Intuitive method names
- Minimal configuration
- Sensible defaults
- Escape hatches for advanced use
## Common Patterns
### Entity Classes
```typescript
class Entity {
constructor(private data: RawData) {}
// Simple getters
getName(): string { return this.data.name || 'Unknown'; }
// Computed properties
getDisplayName(): string { return this.name || this.username; }
// Safe array access
getItems(): Item[] { return this.data.items || []; }
// Date handling
getCreatedAt(): Date { return new Date(this.data.timestamp); }
// Raw data access
get raw(): RawData { return this.data; }
}
```
### Search Results
```typescript
class SearchResult {
constructor(
private data: any,
private api: ApiClient,
private params: SearchParams
) {}
get items(): SearchItem[] { ... }
get total(): number { ... }
hasNextPage(): boolean { ... }
async getNextPage(): Promise<SearchResult> { ... }
}
```
### Error Handling
```typescript
// Specific error types
export class ApiError extends Error {
constructor(message: string, public code: string) {
super(message);
}
}
export class NotFoundError extends ApiError {
constructor(resource: string) {
super(`${resource} not found`, 'NOT_FOUND');
}
}
// Usage
try {
const profile = await api.getProfile('username');
} catch (error) {
if (error instanceof NotFoundError) {
// Handle not found
} else if (error instanceof RateLimitError) {
// Wait and retry
}
}
```
### Helper Functions
```typescript
// Validation
export function isValidUsername(username: string): boolean {
return /^[a-zA-Z0-9-]{3,100}$/.test(username);
}
// Extraction
export function extractUsername(url: string): string | null {
const match = url.match(/linkedin\.com\/in\/([a-zA-Z0-9-]+)/);
return match?.[1] || null;
}
// Defaults
export const DEFAULT_AVATAR = 'https://cdn.linkedin.com/default-profile.png';
```
## Implementation Checklist
### Planning Phase
- [ ] Collect real API response examples
- [ ] Identify all data variations and edge cases
- [ ] Design entity class interfaces
- [ ] Plan helper constants and utilities
- [ ] Consider pagination patterns
- [ ] Plan error handling strategy
### Development Phase
- [ ] Create TypeScript interfaces from real data
- [ ] Implement entity classes with safe defaults
- [ ] Add helper methods for common operations
- [ ] Create search result classes with pagination
- [ ] Implement proper error types
- [ ] Add caching layer (if applicable)
- [ ] Create helper constants
- [ ] Write comprehensive JSDoc comments
### Testing Phase
- [ ] Test with real API responses
- [ ] Verify null/undefined handling
- [ ] Test pagination edge cases
- [ ] Verify TypeScript types are accurate
- [ ] Test error scenarios
- [ ] Ensure documentation examples are accurate
### Documentation Phase
- [ ] Write clear README with inline code examples
- [ ] Document all public methods
- [ ] Include common use cases
- [ ] Document error handling
- [ ] Add troubleshooting section
### Publishing Phase
- [ ] Set correct package.json fields
- [ ] Include only necessary files
- [ ] Test npm installation
- [ ] Verify TypeScript definitions work
- [ ] Tag version appropriately
## Lessons Learned
### What Works Well
1. **Entity classes** - Dramatically simplify data access
2. **Branded types** - Prevent ID mix-ups at compile time
3. **Safe defaults** - Eliminate most runtime errors
4. **Search ā Full data flow** - Intuitive for developers
5. **Helper constants** - Remove magic strings/numbers
### Common Pitfalls to Avoid
1. **Trusting documentation over real data** - Always verify
2. **Exposing raw API complexity** - Abstract it away
3. **Forgetting edge cases** - Empty strings, nulls, missing fields
4. **Inconsistent error handling** - Use typed errors
5. **Missing pagination helpers** - Makes the API painful to use
### Future Improvements
1. **Response validation** - Runtime type checking with zod/joi
2. **Retry strategies** - Exponential backoff for failures
3. **Request queuing** - Respect rate limits proactively
4. **Streaming responses** - For large data sets
5. **Webhook support** - For real-time updates
## Conclusion
This guide represents a battle-tested approach to creating developer-friendly API packages. By starting with real data, hiding complexity behind intuitive interfaces, and prioritizing the developer experience, we create packages that are both powerful and pleasant to use.
Remember: The best API package is one where developers never need to read the original API documentation.