See GraphQL API guide for usage patterns and frontend examples. This doc covers the route handler implementation itself.
POST /api/github/graphql
File: app/api/github/graphql/route.ts (128 lines)
Execution engine: lib/github/graphqlProxy.ts (85 lines)
{
"query": "query ViewerProfile { viewer { login } }",
"variables": { "login": "octocat" },
"operationName": "ViewerProfile"
}| Field | Type | Required | Description |
|---|---|---|---|
query | string | yes* | GraphQL query document (can omit if fallback exists) |
operationName | string | yes | Must match server allowlist |
variables | object | no | Query variables |
Allowlist architecture
graphqlProxy.ts defines the allowed operations:
export const USER_GITHUB_GRAPHQL_OPERATIONS = [
'ViewerProfile',
'OrganizationSummary',
'SearchUsers',
'OrgRepositories',
'RepoScoringData',
'OrgTeamsData',
'OrgScoringData',
'OrganizationRepositories',
];Lookups are O(1) set operations. The check happens before any database or GitHub API call — if the operation isn’t allowed, it fails fast.
Adding a new operation
- Add the name to
USER_GITHUB_GRAPHQL_OPERATIONS - Optionally add a fallback query in the route’s
OPERATION_QUERY_FALLBACKS - Create a frontend query hook in
hooks/queries/
Query resolution
function normalizeQuery(query: unknown): string | null {
if (typeof query !== 'string') return null;
const normalized = query.trim();
return normalized.length ? normalized : null;
}If the normalized query is null, falls back to OPERATION_QUERY_FALLBACKS[operationName]. If there’s no fallback either, returns null → 400.
Sending query: "" triggers the fallback if one exists.
Query fallback example: SearchUsers
If operationName: "SearchUsers" is sent without a query, the route injects:
query SearchUsers($search: String!) {
search(first: 10, type: USER, query: $search) {
nodes {
... on User {
login
avatarUrl
}
}
}
}Resolution logic — only server-owned queries are used; client-supplied query text is ignored:
const FALLBACKS = { SearchUsers: SEARCH_USERS_QUERY };
function resolveQuery(operationName) {
return FALLBACKS[operationName] ?? null;
}Execution
const octokit = createUserOctokit(accessToken); // lib/github/client.ts
const data = await octokit.graphql<T>(query, variables);The access token comes from the decrypted session token.
Response format
Success (200): { "data": { ... } }
Error: { "error": "..." }
| Status | Condition | Source |
|---|---|---|
| 400 | Missing/empty operationName | route.ts:59-60 |
| 400 | Missing/invalid query (no fallback) | route.ts:67-69 |
| 401 | No session | route.ts:82-83 |
| 403 | Operation not in allowlist | route.ts:85-86 |
| 500 | GitHub GraphQL error | route.ts:93-95 |
GitHub API errors (rate limits, validation errors, not found) are logged server-side but returned as generic 500 to prevent information leakage.
Error scenarios
| Scenario | What happens |
|---|---|
| Rate limited by GitHub | octokit.graphql throws → 500. Error logged server-side. |
| Missing field in response | GraphQL returns { data: { viewer: null } }. Route passes through. Frontend handles null. |
| Invalid GraphQL syntax | GitHub returns error → octokit.graphql throws → 500. |
| Wrong variables for operation | GitHub validation error → octokit.graphql throws → 500. |
Related
- GraphQL API Guide — usage, examples, frontend hooks
- API Overview — all endpoints
- Auth Routes — session authentication