Variables
Use GraphQL variables to create dynamic, reusable queries and mutations with full type safety.
Why Use Variables?
Variables allow you to:
- Separate query logic from data
- Reuse queries with different inputs
- Prevent injection attacks
- Enable query caching
Basic Variable Syntax
Use the $ function to declare variables:
import { Chain, $ } from './zeus';
const chain = Chain('https://api.com/graphql');
const result = await chain('query')({
user: [
{
id: $('userId', 'ID!'), // Declare variable
},
{
name: true,
email: true,
},
],
})({
userId: '123', // Provide variable value
});Variable Declaration
$ Function Syntax
$('variableName', 'GraphQLType');- variableName: Variable identifier (string)
- GraphQLType: GraphQL type (with
!for required)
Common Types
// Scalar types
$('id', 'ID!'); // Required ID
$('name', 'String'); // Optional String
$('age', 'Int!'); // Required Int
$('price', 'Float'); // Optional Float
$('active', 'Boolean!'); // Required Boolean
// Custom scalars
$('date', 'DateTime!'); // Required DateTime
$('data', 'JSON'); // Optional JSON
// Enums
$('role', 'UserRole!'); // Required enum
// Input types
$('input', 'CreateUserInput!'); // Required input object
// Arrays
$('ids', '[ID!]!'); // Required array of required IDs
$('tags', '[String!]'); // Optional array of required StringsQuery Variables
Single Variable
const result = await chain('query')({
user: [
{
id: $('userId', 'ID!'),
},
{
id: true,
name: true,
email: true,
},
],
})({
userId: '123',
});Multiple Variables
const result = await chain('query')({
users: [
{
first: $('limit', 'Int!'),
offset: $('offset', 'Int!'),
where: {
role: $('role', 'UserRole'),
},
},
{
id: true,
name: true,
role: true,
},
],
})({
limit: 20,
offset: 0,
role: 'ADMIN',
});Optional Variables
const result = await chain('query')({
users: [
{
first: $('limit', 'Int'), // Optional
where: {
role: $('role', 'UserRole'), // Optional
},
},
{
name: true,
},
],
})({
limit: 10,
// role omitted - uses server default
});Mutation Variables
Create Mutation
const result = await chain('mutation')({
createUser: [
{
input: $('input', 'CreateUserInput!'),
},
{
id: true,
name: true,
email: true,
},
],
})({
input: {
name: 'Zeus',
email: 'zeus@olympus.com',
role: 'ADMIN',
},
});Update Mutation
const result = await chain('mutation')({
updateUser: [
{
id: $('id', 'ID!'),
input: $('input', 'UpdateUserInput!'),
},
{
id: true,
name: true,
updatedAt: true,
},
],
})({
id: '123',
input: {
name: 'Zeus Updated',
},
});Delete Mutation
const result = await chain('mutation')({
deleteUser: [
{
id: $('id', 'ID!'),
},
{
id: true,
name: true,
},
],
})({
id: '123',
});Complex Variables
Nested Input Objects
const result = await chain('mutation')({
createUser: [
{
input: $('input', 'CreateUserInput!'),
},
{
id: true,
name: true,
profile: {
bio: true,
},
},
],
})({
input: {
name: 'Zeus',
email: 'zeus@olympus.com',
profile: {
bio: 'King of the Gods',
location: 'Mount Olympus',
},
},
});Array Variables
const result = await chain('query')({
users: [
{
ids: $('userIds', '[ID!]!'), // Array of IDs
},
{
id: true,
name: true,
},
],
})({
userIds: ['1', '2', '3'],
});Filter Objects
const result = await chain('query')({
posts: [
{
where: $('filter', 'PostFilter'),
},
{
id: true,
title: true,
},
],
})({
filter: {
status: 'PUBLISHED',
featured: true,
author: {
role: 'ADMIN',
},
},
});Reusable Query Functions
Query Builder
function getUser(userId: string) {
return chain('query')({
user: [
{
id: $('userId', 'ID!'),
},
{
id: true,
name: true,
email: true,
},
],
})({
userId,
});
}
const user = await getUser('123');Parameterized Search
type SearchOptions = {
query: string;
limit?: number;
offset?: number;
};
function searchPosts(options: SearchOptions) {
return chain('query')({
searchPosts: [
{
query: $('query', 'String!'),
first: $('limit', 'Int'),
offset: $('offset', 'Int'),
},
{
id: true,
title: true,
excerpt: true,
score: true,
},
],
})({
query: options.query,
limit: options.limit ?? 10,
offset: options.offset ?? 0,
});
}
const results = await searchPosts({
query: 'GraphQL',
limit: 20,
});CRUD Operations
type User = {
id?: string;
name: string;
email: string;
};
const userOperations = {
async getById(id: string) {
return chain('query')({
user: [{ id: $('id', 'ID!') }, { id: true, name: true, email: true }],
})({ id });
},
async create(input: Omit<User, 'id'>) {
return chain('mutation')({
createUser: [{ input: $('input', 'CreateUserInput!') }, { id: true, name: true, email: true }],
})({ input });
},
async update(id: string, input: Partial<User>) {
return chain('mutation')({
updateUser: [
{
id: $('id', 'ID!'),
input: $('input', 'UpdateUserInput!'),
},
{ id: true, name: true, email: true, updatedAt: true },
],
})({ id, input });
},
async delete(id: string) {
return chain('mutation')({
deleteUser: [{ id: $('id', 'ID!') }, { id: true }],
})({ id });
},
};
// Usage
const user = await userOperations.getById('123');
await userOperations.update('123', { name: 'Updated Name' });
await userOperations.delete('123');Default Values
In Query Definition
const result = await chain('query')({
users: [
{
first: $('limit', 'Int') || 10, // Default value
where: {
active: $('active', 'Boolean') ?? true,
},
},
{
name: true,
},
],
})({
// Can omit variables with defaults
});In Function Parameters
function getUsers(limit: number = 10, offset: number = 0) {
return chain('query')({
users: [
{
first: $('limit', 'Int!'),
offset: $('offset', 'Int!'),
},
{
id: true,
name: true,
},
],
})({
limit,
offset,
});
}
// Use defaults
const users = await getUsers();
// Override defaults
const moreUsers = await getUsers(50, 100);Type Safety
Inferred Types
TypeScript infers variable types:
const result = await chain('query')({
user: [
{
id: $('userId', 'ID!'),
},
{
name: true,
},
],
})({
userId: '123', // ✅ String
// userId: 123, // ❌ TypeScript error - number not allowed
});Input Type Validation
import { InputType, GraphQLTypes } from './zeus';
type CreateUserInput = InputType<GraphQLTypes['CreateUserInput']>;
const result = await chain('mutation')({
createUser: [
{
input: $('input', 'CreateUserInput!'),
},
{
id: true,
name: true,
},
],
})({
input: {
name: 'Zeus',
email: 'zeus@olympus.com',
role: 'ADMIN', // ✅ Valid enum value
// role: 'INVALID', // ❌ TypeScript error
} satisfies CreateUserInput,
});Variable Validation
Runtime Validation
function validateUserId(id: string): string {
if (!id || id.length === 0) {
throw new Error('User ID is required');
}
return id;
}
async function getUser(userId: string) {
const validatedId = validateUserId(userId);
return chain('query')({
user: [{ id: $('userId', 'ID!') }, { id: true, name: true }],
})({
userId: validatedId,
});
}Schema Validation
import { z } from 'zod';
const CreateUserSchema = z.object({
name: z.string().min(2).max(50),
email: z.string().email(),
age: z.number().int().min(0).max(150).optional(),
});
async function createUser(input: unknown) {
// Validate input
const validatedInput = CreateUserSchema.parse(input);
return chain('mutation')({
createUser: [{ input: $('input', 'CreateUserInput!') }, { id: true, name: true, email: true }],
})({
input: validatedInput,
});
}
// Usage
try {
await createUser({
name: 'Zeus',
email: 'zeus@olympus.com',
});
} catch (error) {
console.error('Validation failed:', error);
}Dynamic Queries
Conditional Variables
type UserQueryOptions = {
includeProfile?: boolean;
includePosts?: boolean;
postsLimit?: number;
};
function buildUserQuery(userId: string, options: UserQueryOptions = {}) {
const query: any = {
user: [
{ id: $('userId', 'ID!') },
{
id: true,
name: true,
email: true,
},
],
};
// Add profile if requested
if (options.includeProfile) {
query.user[1].profile = {
avatar: true,
bio: true,
};
}
// Add posts if requested
if (options.includePosts) {
query.user[1].posts = [
{
first: $('postsLimit', 'Int'),
},
{
id: true,
title: true,
},
];
}
return chain('query')(query)({
userId,
...(options.postsLimit && { postsLimit: options.postsLimit }),
});
}
// Usage
const user1 = await buildUserQuery('123', { includeProfile: true });
const user2 = await buildUserQuery('123', { includePosts: true, postsLimit: 5 });Variable Field Selection
function getUser(userId: string, fields: string[]) {
const selection = fields.reduce((acc, field) => {
acc[field] = true;
return acc;
}, {} as any);
return chain('query')({
user: [{ id: $('userId', 'ID!') }, selection],
})({
userId,
});
}
const user = await getUser('123', ['id', 'name', 'email']);Performance Optimization
Query Caching
Variables enable server-side query caching:
// This query can be cached by the server
// Only variables change between requests
const getCachedUser = (userId: string) =>
chain('query')({
user: [{ id: $('userId', 'ID!') }, { id: true, name: true, email: true }],
})({ userId });
const user1 = await getCachedUser('123');
const user2 = await getCachedUser('456'); // Same query, different variableBatch Variable Requests
async function getUsersBatch(userIds: string[]) {
const promises = userIds.map((id) =>
chain('query')({
user: [{ id: $('userId', 'ID!') }, { id: true, name: true }],
})({ userId: id }),
);
return Promise.all(promises);
}
const users = await getUsersBatch(['1', '2', '3', '4', '5']);Best Practices
1. Always Use Variables for User Input
// ✅ Safe - uses variables
const result = await chain('query')({
user: [{ id: $('userId', 'ID!') }, { name: true }],
})({ userId: userInput });
// ❌ Unsafe - string interpolation (potential injection)
const result = await chain('query')({
user: [
{ id: userInput }, // Don't do this!
{ name: true },
],
});2. Use TypeScript Types
import { ModelTypes, InputType, GraphQLTypes } from './zeus';
type CreateUserInput = InputType<GraphQLTypes['CreateUserInput']>;
async function createUser(input: CreateUserInput) {
return chain('mutation')({
createUser: [{ input: $('input', 'CreateUserInput!') }, { id: true, name: true }],
})({ input });
}3. Create Reusable Query Functions
// Reusable query functions with variables
const queries = {
getUser: (id: string) =>
chain('query')({
user: [{ id: $('id', 'ID!') }, { id: true, name: true, email: true }],
})({ id }),
listUsers: (limit: number = 10) =>
chain('query')({
users: [{ first: $('limit', 'Int!') }, { id: true, name: true }],
})({ limit }),
};
const user = await queries.getUser('123');
const users = await queries.listUsers(20);4. Document Variable Types
/**
* Searches posts by query string
* @param query - Search query string
* @param limit - Maximum number of results (default: 10)
* @param offset - Number of results to skip (default: 0)
*/
function searchPosts(query: string, limit: number = 10, offset: number = 0) {
return chain('query')({
searchPosts: [
{
query: $('query', 'String!'),
first: $('limit', 'Int!'),
offset: $('offset', 'Int!'),
},
{
id: true,
title: true,
excerpt: true,
},
],
})({ query, limit, offset });
}Next Steps
- Selectors - Reusable field selections
- Type Inference - Deep dive into types
- Basic Queries - Learn query fundamentals
Last updated on