Skip to content

LeetCode Adapter

Platform adapter for fetching contest data from LeetCode GraphQL API.

Overview

The LeetCode adapter integrates with LeetCode's GraphQL API to fetch Weekly and Biweekly contest information.

Status

Enabled - Fully implemented and active

API Details

PropertyValue
API TypeGraphQL
Endpointhttps://leetcode.com/graphql
MethodPOST
AuthenticationNone (public API)
Timeout15000ms
Retry Attempts3

Location

Server/src/integrations/platforms/leetcode/leetcode.adapter.ts

Implementation

typescript
@Injectable()
export class LeetCodeAdapter extends BasePlatformAdapter {
  readonly platformName = ContestPlatform.LEETCODE;
  private readonly graphqlEndpoint = PLATFORM_URLS.LEETCODE;

  constructor() {
    const config: PlatformConfig = {
      enabled: true,
      apiUrl: PLATFORM_URLS.LEETCODE,
      timeout: PLATFORM_TIMEOUTS.LEETCODE,
      retryAttempts: HTTP_CONFIG.DEFAULT_RETRY_ATTEMPTS,
      retryDelay: HTTP_CONFIG.DEFAULT_RETRY_DELAY,
    };
    super(config);
  }
}

GraphQL Schema

Query

graphql
query allContests {
  allContests {
    title
    titleSlug
    startTime
    duration
    originStartTime
    isVirtual
    cardImg
    description
  }
}

Response Type

typescript
interface LeetCodeContest {
  title: string;              // Contest title
  titleSlug: string;          // URL-friendly slug
  startTime: number;          // Unix timestamp
  duration: number;           // Duration in seconds
  originStartTime: number;    // Original start time
  isVirtual: boolean;         // Is virtual contest
  cardImg?: string;           // Contest image URL
  description?: string;       // Contest description
}

interface LeetCodeGraphQLResponse {
  data: {
    allContests?: LeetCodeContest[];
    topTwoContests?: LeetCodeContest[];
  };
}

Contest Types

TypeDescriptionSchedule
WEEKLYWeekly ContestSunday 2:30 AM UTC
BIWEEKLYBiweekly ContestSaturday 2:30 AM UTC

Type Detection:

typescript
let type: ContestType = ContestType.WEEKLY;
if (lcContest.title.toLowerCase().includes('biweekly')) {
  type = ContestType.BIWEEKLY;
} else if (lcContest.title.toLowerCase().includes('weekly')) {
  type = ContestType.WEEKLY;
}

Methods

fetchContests()

Fetch all contests from LeetCode GraphQL API.

typescript
async fetchContests(): Promise<ContestData[]>

Process:

  1. Construct GraphQL query
  2. Make POST request with query
  3. Validate response data
  4. Transform each contest to internal format
  5. Return array of ContestData

Example Response:

json
{
  "data": {
    "allContests": [
      {
        "title": "Weekly Contest 380",
        "titleSlug": "weekly-contest-380",
        "startTime": 1707998400,
        "duration": 5400,
        "originStartTime": 1707998400,
        "isVirtual": false,
        "cardImg": "https://assets.leetcode.com/contest/weekly-380.png",
        "description": "Weekly Contest 380"
      }
    ]
  }
}

fetchUpcomingContests()

Fetch only upcoming contests.

typescript
async fetchUpcomingContests(): Promise<ContestData[]>

fetchRunningContests()

Fetch only running contests.

typescript
async fetchRunningContests(): Promise<ContestData[]>

transformToInternalFormat()

Transform LeetCode contest to unified format.

typescript
transformToInternalFormat(lcContest: LeetCodeContest): ContestData

Transformation Logic:

typescript
{
  platformId: lcContest.titleSlug,
  platform: ContestPlatform.LEETCODE,
  name: lcContest.title,
  type: this.detectType(lcContest.title),
  phase: this.determinePhase(startTime, endTime),
  startTime: new Date(lcContest.startTime * 1000),
  endTime: new Date((lcContest.startTime + lcContest.duration) * 1000),
  durationMinutes: Math.floor(lcContest.duration / 60),
  description: lcContest.description,
  websiteUrl: `https://leetcode.com/contest/${lcContest.titleSlug}`,
  isActive: phase === ContestPhase.BEFORE || phase === ContestPhase.CODING,
  platformMetadata: {
    titleSlug: lcContest.titleSlug,
    isVirtual: lcContest.isVirtual,
    cardImg: lcContest.cardImg,
    originStartTime: lcContest.originStartTime,
  },
  lastSyncedAt: new Date(),
}

makeGraphQLRequest()

Custom GraphQL request handler with retry logic.

typescript
private async makeGraphQLRequest<T>(query: string): Promise<T>

Features:

  • POST request with GraphQL query
  • Custom headers (Content-Type, Origin, Referer)
  • Timeout handling with AbortController
  • Automatic retry on failure
  • Error logging

Headers:

typescript
{
  'Content-Type': 'application/json',
  'User-Agent': 'CodeNotify/1.0',
  'Origin': 'https://leetcode.com',
  'Referer': 'https://leetcode.com/contest/',
}

Platform Metadata

LeetCode-specific metadata stored in platformMetadata:

typescript
{
  titleSlug: string;        // URL slug
  isVirtual: boolean;       // Virtual contest flag
  cardImg: string;          // Contest image URL
  originStartTime: number;  // Original start timestamp
}

Phase Determination

typescript
const now = new Date();
let phase: ContestPhase;

if (startTime > now) {
  phase = ContestPhase.BEFORE;
} else if (endTime > now) {
  phase = ContestPhase.CODING;
} else {
  phase = ContestPhase.FINISHED;
}

Example Usage

Sync LeetCode Contests

bash
# Manual sync (admin only)
curl -X POST http://localhost:3000/contests/sync/leetcode \
  -H "Authorization: Bearer <admin_token>"

Get LeetCode Contests

bash
# Get all LeetCode contests
curl http://localhost:3000/contests/platform/leetcode \
  -H "Authorization: Bearer <token>"

GraphQL Query Example

bash
curl -X POST https://leetcode.com/graphql \
  -H "Content-Type: application/json" \
  -d '{
    "query": "query { allContests { title titleSlug startTime duration } }"
  }'

Error Handling

GraphQL Errors

typescript
if (!response.data?.allContests) {
  this.logger.warn('No contests found in LeetCode response');
  return [];
}

Network Errors

Handled by custom makeGraphQLRequest() with automatic retry (3 attempts).

Timeout

typescript
const controller = new AbortController();
const timeoutId = setTimeout(
  () => controller.abort(),
  this.config.timeout
);

// ... fetch with signal
if (error.name === 'AbortError') {
  throw new Error('LeetCode GraphQL request timed out');
}

Testing

typescript
describe('LeetCodeAdapter', () => {
  let adapter: LeetCodeAdapter;

  beforeEach(() => {
    adapter = new LeetCodeAdapter();
  });

  it('should fetch contests via GraphQL', async () => {
    const contests = await adapter.fetchContests();
    expect(contests).toBeDefined();
    expect(Array.isArray(contests)).toBe(true);
  });

  it('should detect contest type from title', () => {
    const weeklyContest = {
      title: 'Weekly Contest 380',
      titleSlug: 'weekly-contest-380',
      startTime: 1707998400,
      duration: 5400,
      originStartTime: 1707998400,
      isVirtual: false,
    };

    const result = adapter.transformToInternalFormat(weeklyContest);
    expect(result.type).toBe(ContestType.WEEKLY);
  });

  it('should handle biweekly contests', () => {
    const biweeklyContest = {
      title: 'Biweekly Contest 120',
      titleSlug: 'biweekly-contest-120',
      startTime: 1707998400,
      duration: 5400,
      originStartTime: 1707998400,
      isVirtual: false,
    };

    const result = adapter.transformToInternalFormat(biweeklyContest);
    expect(result.type).toBe(ContestType.BIWEEKLY);
  });

  it('should include platform metadata', () => {
    const contest = {
      title: 'Weekly Contest 380',
      titleSlug: 'weekly-contest-380',
      startTime: 1707998400,
      duration: 5400,
      originStartTime: 1707998400,
      isVirtual: false,
      cardImg: 'https://assets.leetcode.com/contest.png',
    };

    const result = adapter.transformToInternalFormat(contest);
    expect(result.platformMetadata).toEqual({
      titleSlug: 'weekly-contest-380',
      isVirtual: false,
      cardImg: 'https://assets.leetcode.com/contest.png',
      originStartTime: 1707998400,
    });
  });
});

Configuration

Environment Variables

bash
# LeetCode GraphQL URL (optional, uses default)
LEETCODE_API_URL=https://leetcode.com/graphql

# Timeout in milliseconds
LEETCODE_TIMEOUT=15000

# Enable/disable adapter
LEETCODE_ENABLED=true

Rate Limiting

LeetCode GraphQL API considerations:

  • Limit: Not publicly documented
  • Handling: Built-in retry with exponential backoff
  • Recommendation: Sync every 6 hours (scheduled)
  • Headers: Include Origin and Referer for better reliability

Best Practices

✅ Do

  1. Use GraphQL queries - More efficient than REST
  2. Include proper headers - Origin, Referer, Content-Type
  3. Handle virtual contests - Check isVirtual flag
  4. Use titleSlug as ID - More stable than numeric IDs
  5. Cache contest images - Store cardImg URLs
  6. Respect API limits - Don't query too frequently

❌ Don't

  1. Don't skip headers - May cause request failures
  2. Don't ignore isVirtual - Virtual contests have different rules
  3. Don't hardcode queries - Use constants or variables
  4. Don't sync continuously - Use scheduled jobs (6 hours)
  5. Don't ignore timeout - GraphQL can be slow

Contest Schedule

Weekly Contest

  • Day: Sunday
  • Time: 2:30 AM UTC
  • Duration: 90 minutes (5400 seconds)
  • Problems: 4 problems

Biweekly Contest

  • Day: Saturday (every 2 weeks)
  • Time: 2:30 AM UTC
  • Duration: 90 minutes (5400 seconds)
  • Problems: 4 problems

Monitoring

Health Check

bash
curl http://localhost:3000/contests/health

Sync Logs

[LeetCodeAdapter] Fetching contests from LeetCode GraphQL API
[LeetCodeAdapter] Successfully fetched 45 contests from LeetCode
[ContestsService] Syncing leetcode: 2 new, 8 updated, 0 failed

Troubleshooting

GraphQL Request Fails

Symptoms: Timeout or 403 errors

Solutions:

  1. Check headers (Origin, Referer)
  2. Verify network connectivity
  3. Check LeetCode API status
  4. Increase timeout if needed

No Contests Returned

Symptoms: Empty array response

Solutions:

  1. Verify GraphQL query syntax
  2. Check response structure
  3. Log raw response for debugging
  4. Verify API endpoint URL

Built with ❤️ for competitive programmers worldwide.