This document explains how the VS Code extension integrates with the Computor backend API.
- Overview
- Backend API Specification
- Authentication
- Using ComputorApiService
- Type Safety
- Error Handling
- Caching
- Examples
The extension communicates with the Computor backend through the ComputorApiService class, which provides a type-safe wrapper around the HTTP client.
Key Components:
- ComputorApiService: Main API service (source)
- HTTP Clients: BasicAuth, ApiKey, and JWT implementations
- Type Definitions: Generated from backend schema (types/generated/)
For the complete backend API specification with all endpoints, request/response models, and HTTP methods, see:
This document is the authoritative source for:
- All available endpoints organized by client (CourseClient, ExampleClient, etc.)
- Request and response model types
- HTTP methods and paths
- Query parameters
The extension uses bearer token authentication with username/password login:
const client = new BearerTokenHttpClient(baseUrl, timeout);
await client.authenticateWithCredentials(username, password);Headers: Authorization: Bearer <access-token>
Features:
- Automatic token refresh using refresh tokens
- Secure token storage in VS Code's secret storage
- Token expiration handling with automatic re-authentication
Authentication Flow:
- User provides username and password
- Extension calls
/auth/loginendpoint - Backend returns access token and refresh token
- Access token is used for API requests
- When token expires, refresh token is used to get new access token
The ComputorApiService is initialized during extension activation:
const api = new ComputorApiService(context);
// HTTP client is injected
(api as any).httpClient = client;The service provides typed methods for common operations:
// Get courses
const courses = await api.getCourses({
organization_id: 'org-123'
});
// Get course content
const content = await api.getCourseContent('content-456');
// create content
const newContent = await api.createCourseContent({
course_id: 'course-789',
title: 'Week 1',
content_type_id: 'type-001'
});
// Update course content
await api.updateCourseContent('content-456', {
title: 'Week 1: Updated'
});
// Delete course content
await api.deleteCourseContent('content-456');For endpoints not yet wrapped in typed methods:
// GET request
const data = await api.get<ResponseType>('/api/endpoint');
// POST request
const result = await api.post<ResponseType>(
'/api/endpoint',
requestBody
);
// PATCH request
await api.patch('/api/endpoint/123', updates);
// DELETE request
await api.delete('/api/endpoint/123');Pass query parameters in the options:
const examples = await api.getExamples({
repository_id: 'repo-123',
category: 'Algorithms',
tags: ['sorting', 'searching']
});For multipart/form-data uploads:
import FormData = require('form-data');
const formData = new FormData();
formData.append('file', fileBuffer, 'example.zip');
formData.append('title', 'Example Title');
const example = await api.uploadExample({
repository_id: 'repo-123',
identifier: 'my-example',
title: 'Example Title',
file: fileBuffer
});For downloading files (e.g., examples):
const download = await api.downloadExample('example-123');
// Returns: { buffer: Buffer, filename: string }All API methods use TypeScript types from types/generated/:
import {
CourseGet,
CourseCreate,
CourseUpdate,
CourseContentGet,
ExampleUploadRequest
} from '../types/generated';
// Fully typed request
const createRequest: CourseContentCreate = {
course_id: 'course-123',
title: 'Assignment 1',
content_type_id: 'type-001'
};
const content: CourseContentGet = await api.createCourseContent(
createRequest
);
// TypeScript ensures type safety
console.log(content.id); // ✓ Valid
console.log(content.invalid); // ✗ Compile errorTypes are organized by domain:
- common.ts: Shared types (pagination, errors)
- courses.ts: Course-related types
- examples.ts: Example repository types
- messages.ts: Message and comment types
- users.ts: User and profile types
- organizations.ts: Organization types
- auth.ts: Authentication types
- tasks.ts: Task and submission types
API calls throw HttpError on failure:
import { HttpError } from '../http/errors/HttpError';
try {
const courses = await api.getCourses();
} catch (error) {
if (error instanceof HttpError) {
console.error('HTTP Error:', error.statusCode, error.message);
switch (error.statusCode) {
case 401:
// Unauthorized - re-authenticate
break;
case 404:
// Not found
break;
case 500:
// Server error
break;
}
} else {
// Network or other error
console.error('Unexpected error:', error);
}
}Use ErrorRecoveryService for automatic retries:
import { errorRecoveryService } from '../services/ErrorRecoveryService';
const data = await errorRecoveryService.executeWithRetry(
async () => await api.getCourses(),
{
maxRetries: 3,
backoffMultiplier: 2,
initialDelay: 1000
}
);Always show user-friendly error messages:
try {
await api.createCourseContent(data);
vscode.window.showInformationMessage('Content created successfully');
} catch (error) {
vscode.window.showErrorMessage(
`Failed to create content: ${error.message}`
);
}The API service uses multi-tier caching to reduce redundant requests:
// First call - fetches from API
const courses1 = await api.getCourses();
// Second call - returns from cache (within TTL)
const courses2 = await api.getCourses();import { multiTierCache } from '../services/CacheService';
// Clear entire cache
multiTierCache.clear();
// Set custom cache entry
const cacheKey = 'custom-data';
multiTierCache.set(cacheKey, data, 300000); // 5 min TTL
// Get from cache
const cached = multiTierCache.get<DataType>(cacheKey);Cache is automatically cleared on mutations:
// This clears relevant cache entries
await api.updateCourseContent('content-123', updates);
// Fresh data on next fetch
const updated = await api.getCourseContent('content-123');async function displayCourses(): Promise<void> {
try {
const courses = await api.getCourses();
for (const course of courses.results) {
console.log(`${course.name} (${course.id})`);
}
} catch (error) {
vscode.window.showErrorMessage(
`Failed to load courses: ${error.message}`
);
}
}async function createContent(
courseId: string,
title: string
): Promise<void> {
await vscode.window.withProgress(
{
location: vscode.ProgressLocation.Notification,
title: 'Creating content...',
cancellable: false
},
async (progress) => {
try {
progress.report({ increment: 0 });
const content = await api.createCourseContent({
course_id: courseId,
title: title
});
progress.report({ increment: 100 });
vscode.window.showInformationMessage(
`Content created: ${content.title}`
);
} catch (error) {
throw error;
}
}
);
}async function uploadExample(
zipPath: string,
metadata: {
repository_id: string;
identifier: string;
title: string;
}
): Promise<void> {
try {
// Read file
const fileBuffer = await fs.promises.readFile(zipPath);
// Upload
const example = await api.uploadExample({
...metadata,
file: fileBuffer
});
vscode.window.showInformationMessage(
`Example uploaded: ${example.title}`
);
} catch (error) {
if (error instanceof HttpError && error.statusCode === 400) {
vscode.window.showErrorMessage(
'Invalid example format. Check meta.yaml file.'
);
} else {
vscode.window.showErrorMessage(
`Upload failed: ${error.message}`
);
}
}
}async function getStudentSubmissions(
courseContentId: string,
courseMemberId: string
): Promise<void> {
try {
const submissions = await api.querySubmissions({
course_content_id: courseContentId,
course_member_id: courseMemberId
});
if (submissions.length === 0) {
vscode.window.showInformationMessage(
'No submissions found'
);
return;
}
for (const submission of submissions) {
console.log(
`Submission ${submission.id}: ` +
`Status=${submission.status}, ` +
`Grade=${submission.grade ?? 'Not graded'}`
);
}
} catch (error) {
vscode.window.showErrorMessage(
`Failed to fetch submissions: ${error.message}`
);
}
}async function batchCreateContent(
courseId: string,
titles: string[]
): Promise<void> {
const results = {
success: 0,
failed: 0,
errors: [] as string[]
};
await vscode.window.withProgress(
{
location: vscode.ProgressLocation.Notification,
title: 'Creating content...',
cancellable: false
},
async (progress) => {
for (let i = 0; i < titles.length; i++) {
const title = titles[i];
progress.report({
increment: (100 / titles.length),
message: `Creating "${title}"...`
});
try {
await errorRecoveryService.executeWithRetry(
async () => await api.createCourseContent({
course_id: courseId,
title: title
}),
{ maxRetries: 2 }
);
results.success++;
} catch (error) {
results.failed++;
results.errors.push(`${title}: ${error.message}`);
}
}
}
);
if (results.failed > 0) {
vscode.window.showWarningMessage(
`Created ${results.success}/${titles.length} items. ` +
`${results.failed} failed.`
);
console.error('Errors:', results.errors);
} else {
vscode.window.showInformationMessage(
`Successfully created ${results.success} items`
);
}
}The extension uses request batching to optimize multiple similar requests:
import { requestBatchingService } from '../services/RequestBatchingService';
// Multiple requests batched automatically
const requests = studentIds.map(id =>
requestBatchingService.addRequest('course-members', { id })
);
const members = await Promise.all(requests);Track API performance with the monitoring service:
import { performanceMonitor } from '../services/PerformanceMonitoringService';
performanceMonitor.startOperation('fetch-courses');
const courses = await api.getCourses();
performanceMonitor.endOperation('fetch-courses');
// Get metrics
const metrics = performanceMonitor.getMetrics('fetch-courses');
console.log('Average duration:', metrics.averageDuration);For testing, create a mock service:
class MockComputorApiService extends ComputorApiService {
async getCourses(): Promise<CourseList> {
return {
count: 2,
results: [
{ id: '1', name: 'Course 1', ... },
{ id: '2', name: 'Course 2', ... }
]
};
}
}
// Use in tests
const mockApi = new MockComputorApiService(mockContext);See test/integration/ for examples of API integration tests.
- Complete Backend API Spec - All endpoints with request/response models
- ComputorApiService Source - Implementation details
- Type Definitions - Generated TypeScript types
- HTTP Clients - HTTP client implementations
- Developer Guide - Development setup and patterns