Skip to main content

GraphQL Client

The GraphQL client provides typed queries and mutations to the Heimdall API.

graphqlRequest

Execute a GraphQL query or mutation, returning the full response including errors.

import { graphqlRequest } from "@/lib/api";
import type { GraphQLResponse } from "@/lib/api";

const response: GraphQLResponse<{ user: User }> = await graphqlRequest<{ user: User }>(
query,
variables
);

Parameters

ParameterTypeRequiredDescription
querystringYesGraphQL query/mutation string
variablesRecord<string, unknown>NoQuery variables
optionsGraphQLRequestOptionsNoRequest options

Options

interface GraphQLRequestOptions {
accessToken?: string;
headers?: Record<string, string>;
}

Response

interface GraphQLResponse<T> {
data?: T;
errors?: Array<{ message: string; path?: string[] }>;
}

Example

const query = `
query GetUser($id: ID!) {
user(id: $id) {
id
name
email
}
}
`;

const response = await graphqlRequest<{ user: User }>(query, { id: "123" });

if (response.errors) {
console.error("GraphQL errors:", response.errors);
return;
}

console.log("User:", response.data?.user);

gql

Helper function that extracts data or throws on errors. Useful when you want exceptions instead of error handling.

import { gql } from "@/lib/api";

const data = await gql<{ user: User }>(query, variables);
// Throws if errors occur

Parameters

Same as graphqlRequest.

Returns

Returns T directly (the data from the response).

Throws

  • Error if the response contains GraphQL errors
  • Error if no data is returned

Examples

Query

const query = `
query GetUsers($limit: Int!) {
users(limit: $limit) {
id
name
}
}
`;

try {
const { users } = await gql<{ users: User[] }>(query, { limit: 10 });
console.log("Users:", users);
} catch (error) {
console.error("Query failed:", error.message);
}

Mutation

const mutation = `
mutation UpdateUser($id: ID!, $name: String!) {
updateUser(id: $id, name: $name) {
id
name
}
}
`;

try {
const { updateUser } = await gql<{ updateUser: User }>(mutation, {
id: "123",
name: "New Name",
});
console.log("Updated user:", updateUser);
} catch (error) {
console.error("Mutation failed:", error.message);
}

With Access Token

const { user } = await gql<{ user: User }>(
query,
{ id: "123" },
{ accessToken: session.accessToken }
);

graphqlConfig

Configuration object for the GraphQL client.

import { graphqlConfig } from "@/lib/api";

console.log(graphqlConfig.graphqlUrl); // "http://localhost:3000/v1/gql"
console.log(graphqlConfig.hasSystemKey); // true/false

Authentication Priority

  1. accessToken option (if provided)
  2. SYSTEM_API_KEY environment variable (if set)
  3. No authentication
// Uses SYSTEM_API_KEY automatically
const response = await graphqlRequest(query, variables);

// Uses provided access token
const response = await graphqlRequest(query, variables, {
accessToken: userToken,
});

Error Handling

With graphqlRequest

const response = await graphqlRequest<{ user: User }>(query, variables);

if (response.errors) {
for (const error of response.errors) {
console.error(`Error at ${error.path?.join(".")}: ${error.message}`);
}
return;
}

// Safe to use response.data
console.log(response.data?.user);

With gql

try {
const data = await gql<{ user: User }>(query, variables);
console.log(data.user);
} catch (error) {
// Handle the first error message
console.error(error.message);
}

Client Info & Audit Logging

extractClientInfo

Extract client information from a Next.js request for audit logging.

import { extractClientInfo } from "@/lib/api";
import { NextRequest } from "next/server";

export async function POST(request: NextRequest) {
const clientInfo = extractClientInfo(request);
// { userAgent: "...", clientIp: "..." }

const data = await gql<{ result: Result }>(
mutation,
variables,
clientInfo // Passed as third parameter
);
}

ClientInfo Type

interface ClientInfo {
userAgent?: string;
clientIp?: string;
}

SOURCE_SERVICE Environment Variable

The SOURCE_SERVICE environment variable identifies which application is making the API request. This is automatically included in GraphQL requests for audit logging.

Set in your .env file:

SOURCE_SERVICE=id          # For Heimdall ID app
SOURCE_SERVICE=backend # For Heimdall Backend
SOURCE_SERVICE=policies # For Policies app
SOURCE_SERVICE=discord_bot # For Discord bot

The gql client automatically reads this variable and includes it in the X-Source-Service header.

Complete API Route Example

import { NextRequest, NextResponse } from "next/server";
import { getServerSession } from "next-auth";
import { authOptions } from "@/lib/auth";
import { gql, extractClientInfo } from "@/lib/api";

export async function POST(request: NextRequest) {
const session = await getServerSession(authOptions);
if (!session?.user?.id) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}

const body = await request.json();
const clientInfo = extractClientInfo(request);

const data = await gql<{ updateProfile: User }>(
`mutation UpdateProfile($input: UpdateProfileInput!) {
updateProfile(input: $input) {
id
name
}
}`,
{ input: { userId: session.user.id, ...body } },
clientInfo
);

return NextResponse.json(data.updateProfile);
}

Common Patterns

Fetching with Loading State

function UserProfile({ userId }: { userId: string }) {
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);

useEffect(() => {
async function fetchUser() {
try {
const { user } = await gql<{ user: User }>(
`query { user(id: "${userId}") { id name email } }`
);
setUser(user);
} catch (e) {
setError(e.message);
} finally {
setLoading(false);
}
}
fetchUser();
}, [userId]);

if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
return <div>{user?.name}</div>;
}