This guide documents the backend API changes from PR #122 (Issue #120) and provides complete migration instructions for any frontend implementation consuming the GitRay backend API.
Scope: This document is frontend-agnostic and covers general API interaction patterns, not specific to the current frontend implementation (which is being replaced).
Key Changes:
- All POST endpoints → GET endpoints with query parameters
- Enhanced pagination support
- Filter parameters flattened to query params
- Improved response structures with nested data
- Multi-tier caching for better performance
- API Endpoint Changes
- Detailed Endpoint Documentation
- 1. GET /api/repositories/commits
- 2. GET /api/repositories/heatmap
- 3. GET /api/repositories/contributors
- 4. GET /api/repositories/churn
- 5. GET /api/repositories/summary
- [6. GET /api/repositories/full-data](#6-get-apirepositories full-data)
- Migration Patterns
- Query Parameter Guidelines
- Response Structure Changes
- Error Handling
- Testing Recommendations
- Common Pitfalls
| Old Endpoint | New Endpoint | Method | Key Differences |
|---|---|---|---|
POST /api/repositories |
GET /api/repositories/commits |
POST→GET | Pagination added |
POST /api/repositories/heatmap |
GET /api/repositories/heatmap |
POST→GET | Query params |
POST /api/repositories/contributors |
GET /api/repositories/contributors |
POST→GET | Filters |
POST /api/repositories/churn |
GET /api/repositories/churn |
POST→GET | Churn filters |
POST /api/repositories/full-data |
GET /api/repositories/full-data |
POST→GET | Pagination |
GET /api/repositories/summary |
GET /api/repositories/summary |
No change | Improved caching |
Purpose: Retrieve paginated commit history for a repository.
Query Parameters:
{
repoUrl: string; // Required - Git repository URL
page?: number; // Optional - Page number (default: 1)
limit?: number; // Optional - Items per page (default: 100)
}Example Request:
GET /api/repositories/commits?repoUrl=https://github.com/jonasyr/gitray.git&page=1&limit=50Response Structure:
{
commits: Commit[]; // Array of commit objects
page: number; // Current page number
limit: number; // Items per page
}Sample Response:
{
"commits": [
{
"sha": "abc123...",
"message": "feat: add new feature",
"author": {
"name": "Jonas",
"email": "jonas@example.com"
},
"date": "2024-12-01T10:30:00Z",
"stats": {
"additions": 150,
"deletions": 30
}
}
],
"page": 1,
"limit": 50
}Migration Example:
// OLD (POST)
const response = await fetch('/api/repositories', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ repoUrl })
});
// NEW (GET)
const params = new URLSearchParams({
repoUrl,
page: '1',
limit: '50'
});
const response = await fetch(`/api/repositories/commits?${params}`);
const { commits, page, limit } = await response.json();Purpose: Retrieve commit activity heatmap data with optional filters.
Query Parameters:
{
repoUrl: string; // Required - Git repository URL
author?: string; // Optional - Filter by single author
authors?: string; // Optional - Comma-separated author list
fromDate?: string; // Optional - Start date (ISO 8601)
toDate?: string; // Optional - End date (ISO 8601)
}Example Request:
GET /api/repositories/heatmap?repoUrl=https://github.com/user/repo.git&fromDate=2024-01-01&toDate=2024-12-31Response Structure:
{
heatmapData: {
timePeriod: 'day' | 'week' | 'month';
data: Array<{
date: string; // ISO 8601 date
count: number; // Commit count
authors: number; // Unique author count
}>;
metadata?: {
totalCommits: number;
dateRange: { start: string; end: string };
};
}
}Sample Response:
{
"heatmapData": {
"timePeriod": "day",
"data": [
{ "date": "2024-01-01", "count": 5, "authors": 2 },
{ "date": "2024-01-02", "count": 3, "authors": 1 }
],
"metadata": {
"totalCommits": 480,
"dateRange": {
"start": "2024-01-01",
"end": "2024-12-31"
}
}
}
}Migration Example:
// OLD (POST with nested filterOptions)
const response = await fetch('/api/repositories/heatmap', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
repoUrl,
filterOptions: {
author: 'john',
fromDate: '2024-01-01',
toDate: '2024-12-31'
}
})
});
// NEW (GET with flat query params)
const params = new URLSearchParams({ repoUrl });
if (author) params.append('author', author);
if (fromDate) params.append('fromDate', fromDate);
if (toDate) params.append('toDate', toDate);
const response = await fetch(`/api/repositories/heatmap?${params}`);
const { heatmapData } = await response.json();Purpose: Retrieve all unique contributors without statistics or ranking (GDPR-compliant).
Query Parameters:
{
repoUrl: string; // Required - Git repository URL
author?: string; // Optional - Filter by single author
authors?: string; // Optional - Comma-separated author list
fromDate?: string; // Optional - Start date (ISO 8601)
toDate?: string; // Optional - End date (ISO 8601)
}Example Request:
GET /api/repositories/contributors?repoUrl=https://github.com/user/repo.git&fromDate=2024-01-01Response Structure:
{
contributors: Array<{
login: string; // Author name (GDPR-compliant pseudonymized identifier)
}>
}Sample Response:
{
"contributors": [
{ "login": "Alice" },
{ "login": "Bob" },
{ "login": "Charlie" }
]
}Migration Example:
// OLD (POST)
const response = await fetch('/api/repositories/contributors', {
method: 'POST',
body: JSON.stringify({ repoUrl, filterOptions })
});
// NEW (GET)
const params = new URLSearchParams({ repoUrl });
if (fromDate) params.append('fromDate', fromDate);
if (toDate) params.append('toDate', toDate);
const response = await fetch(`/api/repositories/contributors?${params}`);
const { contributors } = await response.json();
// Note: Contributors now contain only { login: string }, no statisticsIMPORTANT CHANGES (Issue #121):
- Returns all unique contributors, not just top 5
- No commit counts, line statistics, or contribution percentages
- Alphabetically sorted for consistency
- Fully GDPR-compliant (only author names, no tracking metrics)
Purpose: Retrieve code churn analysis showing file change frequency.
Query Parameters:
{
repoUrl: string; // Required - Git repository URL
fromDate?: string; // Optional - Analysis start date (ISO 8601)
toDate?: string; // Optional - Analysis end date (ISO 8601)
minChanges?: string; // Optional - Minimum changes filter (numeric)
extensions?: string; // Optional - Comma-separated file extensions (e.g., 'ts,tsx,js')
}Example Request:
GET /api/repositories/churn?repoUrl=https://github.com/user/repo.git&minChanges=10&extensions=ts,tsxResponse Structure:
{
churnData: {
files: Array<{
path: string;
additions: number;
deletions: number;
changes: number;
riskLevel: 'low' | 'medium' | 'high' | 'critical';
}>;
summary: {
totalFiles: number;
highRiskFiles: number;
averageChanges: number;
};
metadata: {
dateRange: { start: string; end: string };
filters: {
minChanges?: number;
extensions?: string[];
};
};
}
}Sample Response:
{
"churnData": {
"files": [
{
"path": "src/services/cache.ts",
"additions": 450,
"deletions": 120,
"changes": 570,
"riskLevel": "high"
}
],
"summary": {
"totalFiles": 87,
"highRiskFiles": 12,
"averageChanges": 45.3
}
}
}Migration Example:
// OLD (POST)
const response = await fetch('/api/repositories/churn', {
method: 'POST',
body: JSON.stringify({ repoUrl, filterOptions })
});
// NEW (GET with churn-specific params)
const params = new URLSearchParams({ repoUrl });
if (minChanges) params.append('minChanges', minChanges.toString());
if (extensions && extensions.length > 0) {
params.append('extensions', extensions.join(','));
}
if (fromDate) params.append('fromDate', fromDate);
const response = await fetch(`/api/repositories/churn?${params}`);
const { churnData } = await response.json();Purpose: Retrieve repository metadata and statistics.
Query Parameters:
{
repoUrl: string; // Required - Git repository URL
}Example Request:
GET /api/repositories/summary?repoUrl=https://github.com/jonasyr/gitray.gitResponse Structure:
{
summary: {
repository: {
name: string;
owner: string;
url: string;
platform: 'github' | 'gitlab' | 'bitbucket' | 'other';
defaultBranch?: string;
};
created: {
date: string; // ISO 8601
source: 'git-log' | 'github-api' | 'gitlab-api' | 'estimated';
};
age: {
years: number;
months: number;
formatted: string; // e.g., "2.5y"
};
lastCommit: {
date: string; // ISO 8601
relativeTime: string; // e.g., "2 days ago"
sha: string;
author: string;
};
stats: {
totalCommits: number; // ⚠️ Important: nested under stats
contributors: number; // ⚠️ Important: nested under stats
status: 'active' | 'inactive' | 'archived';
};
metadata: {
cached: boolean;
dataSource: 'git-sparse-clone' | 'cache';
createdDateAccuracy: 'exact' | 'approximate';
bandwidthSaved?: string;
lastUpdated: string; // ISO 8601
};
}
}Sample Response:
{
"summary": {
"repository": {
"name": "gitray",
"owner": "jonasyr",
"url": "https://github.com/jonasyr/gitray.git",
"platform": "github"
},
"stats": {
"totalCommits": 480,
"contributors": 6,
"status": "active"
},
"lastCommit": {
"date": "2024-12-02T08:15:00Z",
"relativeTime": "2 hours ago",
"sha": "abc123def456",
"author": "Jonas"
},
"metadata": {
"cached": true,
"dataSource": "cache"
}
}
}// ❌ WRONG - Old structure (will be undefined)
const totalCommits = response.totalCommits;
const contributors = response.totalContributors;
// ✅ CORRECT - New nested structure
const totalCommits = response.summary.stats.totalCommits;
const contributors = response.summary.stats.contributors; // Note: field is 'contributors', not 'totalContributors'Purpose: Retrieve both commits and heatmap data in a single request with pagination and filters.
Query Parameters:
{
repoUrl: string; // Required - Git repository URL
page?: number; // Optional - Page number (default: 1)
limit?: number; // Optional - Items per page (default: 100)
author?: string; // Optional - Filter by single author
authors?: string; // Optional - Comma-separated author list
fromDate?: string; // Optional - Start date (ISO 8601)
toDate?: string; // Optional - End date (ISO 8601)
}Example Request:
GET /api/repositories/full-data?repoUrl=https://github.com/user/repo.git&page=1&limit=20&fromDate=2024-01-01Response Structure:
{
commits: Commit[]; // Paginated commits
heatmapData: CommitHeatmapData; // Filtered heatmap data
page: number;
limit: number;
isValidHeatmap: boolean; // Backend validation flag
}Sample Response:
{
"commits": [
{
"sha": "abc123",
"message": "Initial commit",
"author": { "name": "Jonas", "email": "jonas@example.com" },
"date": "2024-01-01T10:00:00Z"
}
],
"heatmapData": {
"timePeriod": "day",
"data": [
{ "date": "2024-01-01", "count": 1, "authors": 1 }
]
},
"page": 1,
"limit": 20,
"isValidHeatmap": true
}Migration Example:
// OLD (POST)
const response = await fetch('/api/repositories/full-data', {
method: 'POST',
body: JSON.stringify({
repoUrl,
timePeriod: 'month',
filterOptions: { fromDate, toDate }
})
});
// NEW (GET)
const params = new URLSearchParams({
repoUrl,
page: '1',
limit: '100'
});
if (fromDate) params.append('fromDate', fromDate);
if (toDate) params.append('toDate', toDate);
const response = await fetch(`/api/repositories/full-data?${params}`);
const { commits, heatmapData, page, limit } = await response.json();// Before
async function fetchData(repoUrl: string) {
const response = await apiClient.post('/api/repositories', { repoUrl });
return response.data;
}
// After
async function fetchData(repoUrl: string) {
const params = new URLSearchParams({ repoUrl });
const response = await apiClient.get('/api/repositories/commits', { params });
return response.data;
}function buildQueryParams(
repoUrl: string,
filters?: {
author?: string;
authors?: string[];
fromDate?: string;
toDate?: string;
}
): URLSearchParams {
const params = new URLSearchParams({ repoUrl });
if (filters?.author) {
params.append('author', filters.author);
}
if (filters?.authors && filters.authors.length > 0) {
params.append('authors', filters.authors.join(','));
}
if (filters?.fromDate) {
params.append('fromDate', filters.fromDate);
}
if (filters?.toDate) {
params.append('toDate', filters.toDate);
}
return params;
}
// Usage
const params = buildQueryParams(repoUrl, { fromDate: '2024-01-01' });
const response = await fetch(`/api/repositories/heatmap?${params}`);interface PaginationParams {
page?: number;
limit?: number;
}
function addPaginationParams(
params: URLSearchParams,
pagination?: PaginationParams
): void {
const page = pagination?.page ?? 1;
const limit = pagination?.limit ?? 100;
params.append('page', page.toString());
params.append('limit', limit.toString());
}
// Usage
const params = new URLSearchParams({ repoUrl });
addPaginationParams(params, { page: 2, limit: 50 });
const response = await fetch(`/api/repositories/commits?${params}`);async function fetchWithErrorHandling<T>(
endpoint: string,
params: URLSearchParams
): Promise<T> {
try {
const response = await fetch(`${endpoint}?${params}`);
if (!response.ok) {
const error = await response.json();
throw new Error(error.message || `HTTP ${response.status}`);
}
return await response.json();
} catch (error) {
console.error(`Failed to fetch ${endpoint}:`, error);
throw error;
}
}
// Usage
const params = new URLSearchParams({ repoUrl });
const data = await fetchWithErrorHandling('/api/repositories/summary', params);Convert arrays to comma-separated strings:
// Array to comma-separated string
const authors = ['alice', 'bob', 'charlie'];
params.append('authors', authors.join(',')); // 'alice,bob,charlie'
const extensions = ['ts', 'tsx', 'js'];
params.append('extensions', extensions.join(',')); // 'ts,tsx,js'Use ISO 8601 format:
// Correct date formats
params.append('fromDate', '2024-01-01');
params.append('toDate', '2024-12-31');
// Also accepts full ISO 8601
params.append('fromDate', '2024-01-01T00:00:00Z');Convert numbers to strings:
params.append('page', page.toString());
params.append('limit', limit.toString());
params.append('minChanges', minChanges.toString());Only include defined values:
// Good - only includes defined values
if (author) params.append('author', author);
if (fromDate) params.append('fromDate', fromDate);
// Bad - includes undefined
params.append('author', author || ''); // ❌ Don't do thisCritical: The summary endpoint now returns deeply nested data.
// ❌ WRONG - Old pattern (undefined)
interface OldResponse {
totalCommits: number;
totalContributors: number;
status: string;
}
// ✅ CORRECT - New pattern
interface NewResponse {
summary: {
repository: { name: string; owner: string; url: string; platform: string };
stats: {
totalCommits: number; // Access via response.summary.stats.totalCommits
contributors: number; // Note: 'contributors' not 'totalContributors'
status: string;
};
lastCommit: { date: string; sha: string; author: string };
metadata: { cached: boolean };
};
}
// Migration example
function getTotalCommits(response: NewResponse): number {
return response.summary?.stats?.totalCommits ?? 0;
}// Backend returns this structure
interface HeatmapResponse {
heatmapData: {
timePeriod: string;
data: Array<{ date: string; count: number }>;
metadata?: { totalCommits: number };
};
}
// Access pattern
const dataPoints = response.heatmapData.data.length;
const totalCommits = response.heatmapData.metadata?.totalCommits;interface FullDataResponse {
commits: Commit[];
heatmapData: CommitHeatmapData;
isValidHeatmap: boolean; // Backend validation result
}
// Always check validation flag
if (response.isValidHeatmap) {
renderHeatmap(response.heatmapData);
} else {
console.warn('Invalid heatmap data structure');
}| Code | Meaning | Common Causes |
|---|---|---|
400 |
Bad Request | Missing repoUrl, invalid date format, invalid URL |
404 |
Not Found | Wrong endpoint path, typo in URL |
422 |
Validation Error | Invalid query parameter values |
500 |
Server Error | Cache failure, Git operation error |
504 |
Gateway Timeout | Large repository taking too long |
// Example validation error response
{
"error": "Validation failed",
"details": [
{
"field": "repoUrl",
"message": "Invalid URL format"
},
{
"field": "fromDate",
"message": "Invalid date format, use YYYY-MM-DD"
}
]
}async function handleApiCall<T>(
endpoint: string,
params: URLSearchParams
): Promise<T | null> {
try {
const response = await fetch(`${endpoint}?${params}`);
if (response.status === 400) {
const error = await response.json();
console.error('Validation error:', error.details);
return null;
}
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return await response.json();
} catch (error) {
console.error('API call failed:', error);
return null;
}
}Use the GitRay repository for testing:
curl "http://localhost:3001/api/repositories/summary?repoUrl=https://github.com/jonasyr/gitray.git"Expected Results:
stats.totalCommits: 480stats.contributors: 6stats.status: "active"
# Page 1
curl "http://localhost:3001/api/repositories/commits?repoUrl=https://github.com/jonasyr/gitray.git&page=1&limit=10"
# Page 2
curl "http://localhost:3001/api/repositories/commits?repoUrl=https://github.com/jonasyr/gitray.git&page=2&limit=10"# Date range filter
curl "http://localhost:3001/api/repositories/heatmap?repoUrl=https://github.com/jonasyr/gitray.git&fromDate=2024-01-01&toDate=2024-12-31"
# Author filter
curl "http://localhost:3001/api/repositories/contributors?repoUrl=https://github.com/jonasyr/gitray.git&author=jonas"
# Multiple authors
curl "http://localhost:3001/api/repositories/heatmap?repoUrl=https://github.com/jonasyr/gitray.git&authors=jonas,contributor2"# Missing repoUrl
curl "http://localhost:3001/api/repositories/summary"
# Expected: HTTP 400
# Invalid date
curl "http://localhost:3001/api/repositories/heatmap?repoUrl=https://github.com/jonasyr/gitray.git&fromDate=invalid"
# Expected: HTTP 400- All endpoints return HTTP 200 with valid params
- Pagination works correctly (page 1, 2, 3)
- Date filters reduce result set appropriately
- Author filters return subset of commits
- Multiple authors filter works (comma-separated)
- Invalid parameters return HTTP 400
- Missing
repoUrlreturns HTTP 400 - Response structures match documented types
-
summary.stats.totalCommitsaccessible and correct - Heatmap data has
timePeriodanddatafields - Full-data returns both
commitsandheatmapData
// ❌ WRONG - Will get HTTP 404
fetch('/api/repositories/commits', {
method: 'POST',
body: JSON.stringify({ repoUrl })
});
// ✅ CORRECT
const params = new URLSearchParams({ repoUrl });
fetch(`/api/repositories/commits?${params}`);// ❌ WRONG - Returns undefined
const commits = response.totalCommits;
// ✅ CORRECT - Access nested field
const commits = response.summary.stats.totalCommits;// ❌ WRONG - Field doesn't exist
const count = response.summary.stats.totalContributors;
// ✅ CORRECT - Field is 'contributors'
const count = response.summary.stats.contributors;// ❌ WRONG - Don't stringify arrays
params.append('authors', JSON.stringify(['alice', 'bob']));
// ✅ CORRECT - Comma-separated string
params.append('authors', ['alice', 'bob'].join(','));// ❌ WRONG - Includes undefined
params.append('author', author); // If author is undefined
// ✅ CORRECT - Conditional inclusion
if (author) params.append('author', author);// ❌ WRONG - Invalid format
params.append('fromDate', '12/01/2024');
// ✅ CORRECT - ISO 8601 format
params.append('fromDate', '2024-12-01');The backend uses multi-tier caching:
- Memory tier: ~1ms response time
- Disk tier: ~10-50ms response time
- Redis tier: ~50-100ms response time
- Git clone: 5-30 seconds (first request only)
Recommendations:
- First request will be slow (Git clone)
- Subsequent requests with same parameters are fast (cache hit)
- Different filter combinations create separate cache entries
- Don't make unnecessary duplicate requests
// Good - Use reasonable page sizes
const limit = 50; // ✅ Balanced
// Avoid - Too small or too large
const limit = 1; // ❌ Too many requests
const limit = 10000; // ❌ Memory issuesUse this checklist when migrating your frontend:
- Changed all POST requests to GET
- Updated endpoint paths (
/repositories→/repositories/commits) - Moved request body to query parameters
- Arrays converted to comma-separated strings
- Dates in ISO 8601 format (
YYYY-MM-DD) - Numbers converted to strings for query params
- Conditional parameters only included if defined
- Updated to access
response.summary.stats.totalCommits - Using
contributorsinstead oftotalContributors - Handling nested
summaryobject structure - Validating
isValidHeatmapflag in full-data endpoint
- Handling HTTP 400 for validation errors
- Handling HTTP 404 for incorrect endpoints
- Graceful degradation on server errors
- Logging errors for debugging
- Tested all endpoints with valid parameters
- Tested pagination (multiple pages)
- Tested filters (author, date range)
- Tested error cases (missing params, invalid format)
- Verified response structures match documented types
- Backend Repository Routes:
apps/backend/src/routes/repositoryRoutes.ts - Shared Types Package:
packages/shared-types/src/index.ts - API Test Script:
test-api-phase1.sh - Test Scenarios Documentation:
scripts/api_test_scenarios.md
If you encounter problems during migration:
- Check backend logs - Detailed error messages are logged
- Verify query parameters - Use browser DevTools Network tab
- Test with curl - Isolate frontend vs backend issues
- Review response structure - Compare against documented types
- Check SonarQube - Code quality issues may surface
For the most up-to-date backend implementation, always refer to the source code in apps/backend/src/routes/repositoryRoutes.ts.