GraphQL Interview Questions (Free Preview)
Free sample of 15 from 73 questions available
Schema Definition Language (SDL)
What is the difference between [String]!, [String!], and [String!]!?
The 30-Second Answer: These represent different nullability contracts for lists: [String]! is a non-null list that can contain null strings, [String!] is a nullable list of non-null strings, and [String!]! is a non-null list of non-null strings. Understanding these combinations is critical for API design and client-side type safety.
The 2-Minute Answer (If They Want More): GraphQL's list nullability has three levels: the list itself, the items within the list, and combinations of both. Each exclamation mark placement creates different guarantees about what values can be null.
[String]! means the list itself is required and will always be returned (possibly empty), but individual strings inside can be null. You might receive ["Alice", null, "Bob"]. This pattern is useful when the collection always exists but some items might be missing or invalid.
[String!] means the list itself is optional and might be null, but if a list is returned, all strings inside are guaranteed to be non-null. You'll get either null or ["Alice", "Bob"], never ["Alice", null, "Bob"]. This is less common but useful when the entire collection might not apply.
[String!]! provides the strongest guarantee: the list always exists and all items are always non-null. You'll always get a list (possibly empty) with no null items: [] or ["Alice", "Bob"]. This is the most common pattern for collections of required data.
The choice impacts error handling: if a non-null constraint is violated, the error bubbles up to the nearest nullable parent. With [String!]!, a null item causes the entire list to become null (and might propagate further). Understanding these semantics helps you design robust schemas that handle failures gracefully.
Code Example:
type User {
# [String]! - Non-null list, nullable items
tags: [String]!
# Valid: []
# Valid: ["programming", "graphql"]
# Valid: ["programming", null, "graphql"]
# Invalid: null
# [String!] - Nullable list, non-null items
middleNames: [String!]
# Valid: null
# Valid: []
# Valid: ["Marie", "Elizabeth"]
# Invalid: ["Marie", null, "Elizabeth"]
# [String!]! - Non-null list, non-null items
roles: [String!]!
# Valid: []
# Valid: ["admin", "user"]
# Invalid: null
# Invalid: ["admin", null, "user"]
}
type Post {
id: ID!
# Common pattern: non-null list of non-null references
comments: [Comment!]! # Always returns list, never null items
# Nullable list of non-null references
relatedPosts: [Post!] # Might not have related posts (null)
# But if present, all posts are valid
# Non-null list of nullable references
authors: [User]! # Always has authors list
# But some authors might be deleted (null)
}
# Nested list nullability
type MultiDimensional {
# Non-null outer list, nullable inner lists, non-null strings
matrix: [[String!]]!
# Valid: []
# Valid: [["a", "b"], null, ["c"]]
# Invalid: null
# Invalid: [["a", null]]
}
# Query return types
type Query {
# Most common: always return list of valid users
users: [User!]!
# Search might return null if no results concept needed
searchUsers(query: String!): [User!]
# Tags with possible null values (soft-deleted tags?)
postTags(postId: ID!): [String]!
}
References:
↑ Back to topWhat is the difference between type and input type?
The 30-Second Answer: Object types define the structure of data returned from queries and mutations, while input types define the structure of complex arguments passed into queries and mutations. Input types cannot have field arguments or implement interfaces, ensuring they represent pure data structures.
The 2-Minute Answer (If They Want More):
The distinction between type and input is fundamental to GraphQL's architecture. Regular object types (declared with type) represent data that flows from server to client—query results, mutation payloads, and subscription events. They can have field resolvers, accept arguments on individual fields, implement interfaces, and reference other object types.
Input types (declared with input) represent data flowing from client to server as arguments. They're used when you need to pass complex, nested data structures to queries or mutations. Input types have important restrictions: their fields cannot have arguments, they cannot implement interfaces, and they can only reference scalars, enums, other input types, and lists of these.
This separation enforces a clear data flow direction and prevents circular dependencies that could occur if the same type was used for both input and output. It also allows inputs and outputs to evolve independently—for example, a User type might have a posts field with arguments (output only), while a CreateUserInput might have fields specific to user creation.
In practice, you'll often have parallel type hierarchies: User and CreateUserInput, Post and UpdatePostInput. While this creates some duplication, it provides flexibility, clearer intent, and prevents coupling between your API's input and output shapes.
Code Example:
# Regular object type (OUTPUT - server to client)
type User {
id: ID!
name: String!
email: String!
posts(limit: Int): [Post!]! # Field can have arguments
createdAt: DateTime!
}
# Input type (INPUT - client to server)
input CreateUserInput {
name: String!
email: String!
password: String!
# No field arguments allowed
# No interface implementation
# Only scalars, enums, other inputs
}
input UpdateUserInput {
name: String
email: String
# Fields are typically optional for updates
}
# Input types can be nested
input CreatePostInput {
title: String!
content: String!
author: UserReferenceInput! # Nested input type
tags: [String!]!
}
input UserReferenceInput {
id: ID!
}
# Mutations use input types for complex arguments
type Mutation {
createUser(input: CreateUserInput!): User!
updateUser(id: ID!, input: UpdateUserInput!): User
createPost(input: CreatePostInput!): Post!
}
References:
↑ Back to topTools and Ecosystem
What is Apollo Client and how does it work?
The 30-Second Answer: Apollo Client is a comprehensive state management library for JavaScript that manages both local and remote data with GraphQL. It automatically caches query results, handles loading and error states, provides optimistic UI updates, and works with any JavaScript frontend framework (React, Vue, Angular) while requiring minimal configuration.
The 2-Minute Answer (If They Want More): Apollo Client serves as both a GraphQL client and a state management solution, eliminating the need for separate tools like Redux in many cases. When you execute a query, Apollo Client sends it to your GraphQL server, normalizes the response data, stores it in its cache, and automatically updates any UI components that depend on that data. The cache is intelligent—it stores objects by ID and type, enabling automatic cache updates when mutations occur.
The library works through a series of key components: the ApolloClient instance manages the connection to your GraphQL server and cache configuration; the InMemoryCache stores data and handles normalization; Links define the network layer and can chain middleware for authentication, error handling, or batching; and React hooks (useQuery, useMutation, useSubscription) provide the interface for components to interact with GraphQL operations.
Apollo Client excels at optimistic UI updates, where you can immediately update the UI before the server responds, then reconcile when the actual response arrives. It supports pagination with fetchMore, refetching queries, polling for updates, and local state management using reactive variables or the @client directive. The DevTools extension provides powerful debugging capabilities, showing cache contents, query history, and performance metrics.
The cache normalization is particularly powerful—instead of storing query results as trees, Apollo Client breaks objects into individual entities stored by their __typename and id, allowing different queries to share cached data and mutations to automatically update all relevant queries. This reduces network requests and keeps your UI consistent.
Code Example:
// Apollo Client setup and usage in React
import { ApolloClient, InMemoryCache, ApolloProvider, gql, useQuery, useMutation } from '@apollo/client';
// 1. Create Apollo Client instance
const client = new ApolloClient({
uri: 'https://api.example.com/graphql',
cache: new InMemoryCache({
typePolicies: {
Query: {
fields: {
// Custom merge function for pagination
users: {
keyArgs: false,
merge(existing = [], incoming) {
return [...existing, ...incoming];
},
},
},
},
},
}),
// Add authentication
headers: {
authorization: localStorage.getItem('token') || '',
},
});
// 2. Define GraphQL operations
const GET_USERS = gql`
query GetUsers {
users {
id
name
email
}
}
`;
const CREATE_USER = gql`
mutation CreateUser($name: String!, $email: String!) {
createUser(name: $name, email: $email) {
id
name
email
}
}
`;
// 3. React component using Apollo Client hooks
function UserList() {
// Query hook - handles loading, error, and data states
const { loading, error, data, refetch } = useQuery(GET_USERS, {
pollInterval: 30000, // Poll every 30 seconds
fetchPolicy: 'cache-and-network', // Use cache but also fetch fresh data
});
// Mutation hook with cache update
const [createUser, { loading: mutationLoading }] = useMutation(CREATE_USER, {
update(cache, { data: { createUser } }) {
// Read current cache
const { users } = cache.readQuery({ query: GET_USERS });
// Write updated cache
cache.writeQuery({
query: GET_USERS,
data: { users: [...users, createUser] },
});
},
// Optimistic response for instant UI update
optimisticResponse: {
createUser: {
__typename: 'User',
id: 'temp-id',
name: 'Loading...',
email: 'loading@example.com',
},
},
});
const handleCreateUser = () => {
createUser({
variables: {
name: 'New User',
email: 'newuser@example.com',
},
});
};
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error.message}</p>;
return (
<div>
<h2>Users</h2>
<button onClick={handleCreateUser} disabled={mutationLoading}>
Add User
</button>
<button onClick={() => refetch()}>Refresh</button>
<ul>
{data.users.map(user => (
<li key={user.id}>
{user.name} - {user.email}
</li>
))}
</ul>
</div>
);
}
// 4. Wrap app with ApolloProvider
function App() {
return (
<ApolloProvider client={client}>
<UserList />
</ApolloProvider>
);
}
export default App;
References:
↑ Back to topAuthentication and Authorization
What is the difference between authentication and authorization in GraphQL?
The 30-Second Answer: Authentication verifies who you are (identity), while authorization determines what you can do (permissions). In GraphQL, authentication typically happens in the context initialization before resolvers run, while authorization is checked within resolvers or using schema directives to control access to specific fields and types based on the authenticated user's roles and permissions.
The 2-Minute Answer (If They Want More): Authentication and authorization are two distinct security layers that work together. Authentication answers "Who are you?" - it's the process of verifying a user's identity through credentials like passwords, tokens, or biometrics. This happens once per request, typically in middleware or the GraphQL context function, and results in identifying the current user.
Authorization answers "What are you allowed to do?" - it's the process of checking whether the authenticated user has permission to perform a specific action or access specific data. In GraphQL, this happens at multiple levels: query-level (can this user execute this query?), field-level (can this user see this field?), and data-level (can this user access this specific record?).
A common pattern is to authenticate at the transport layer (context initialization) and authorize in business logic (resolvers). For example, you might authenticate a user via JWT in the context, then in your resolver check if that user has the "admin" role before allowing them to delete a post. This separation of concerns makes your code more maintainable and testable.
GraphQL's fine-grained nature makes authorization especially important because clients can request any combination of fields. You can't rely on endpoint-level security like in REST - you need field-level authorization to prevent unauthorized data access. This is why many GraphQL implementations use schema directives (like @auth or @hasRole) to declaratively specify authorization rules directly in the schema.
Code Example:
// Authentication vs Authorization example
// AUTHENTICATION - Happens in context (who are you?)
const server = new ApolloServer({
typeDefs,
resolvers,
context: ({ req }) => {
const token = req.headers.authorization || '';
try {
// AUTHENTICATION: Verify identity
const user = jwt.verify(token.replace('Bearer ', ''), process.env.JWT_SECRET);
return { user }; // User is authenticated
} catch (error) {
return {}; // User is not authenticated
}
}
});
// AUTHORIZATION - Happens in resolvers (what can you do?)
const resolvers = {
Query: {
// Public query - no authorization needed
publicPosts: () => {
return getPublicPosts();
},
// Requires authentication - any logged-in user
myProfile: (parent, args, { user }) => {
if (!user) {
throw new Error('Authentication required');
}
return getUserProfile(user.id);
},
// Requires specific authorization - admin only
allUsers: (parent, args, { user }) => {
if (!user) {
throw new Error('Authentication required');
}
// AUTHORIZATION: Check permissions
if (!user.roles.includes('ADMIN')) {
throw new Error('Authorization failed: Admin access required');
}
return getAllUsers();
}
},
Mutation: {
deletePost: (parent, { postId }, { user }) => {
if (!user) {
throw new Error('Authentication required');
}
const post = getPostById(postId);
// AUTHORIZATION: User can only delete their own posts
// unless they're an admin
if (post.authorId !== user.id && !user.roles.includes('ADMIN')) {
throw new Error('Authorization failed: You can only delete your own posts');
}
return deletePostById(postId);
}
},
User: {
// Field-level authorization
email: (user, args, { user: currentUser }) => {
// Users can only see their own email or admins can see all
if (currentUser?.id === user.id || currentUser?.roles.includes('ADMIN')) {
return user.email;
}
return null; // Hide email from unauthorized users
}
}
};
Mermaid Diagram:
flowchart TD
A[GraphQL Request] --> B{Authentication<br/>Who are you?}
B -->|Valid Token| C[User Identity Established]
B -->|Invalid/No Token| D[Anonymous User]
C --> E{Authorization<br/>What can you access?}
D --> E
E -->|Check Permissions| F{Has Required Role?}
F -->|Yes| G[Execute Resolver]
F -->|No| H[Authorization Error]
E -->|Check Ownership| I{Owns Resource?}
I -->|Yes| G
I -->|No| H
G --> J[Return Data]
H --> K[Return Error]
References:
↑ Back to topSchema Design
What is Apollo Federation and how does it work?
The 30-Second Answer: Apollo Federation is a GraphQL architecture that lets you compose multiple GraphQL services (subgraphs) into a single unified API (supergraph). A gateway routes queries to the appropriate services and combines results. Each service owns specific types and fields, enabling teams to develop independently while presenting clients with one cohesive schema.
The 2-Minute Answer (If They Want More): Apollo Federation solves the problem of scaling GraphQL across multiple teams and services. Instead of building one monolithic GraphQL server, you create multiple specialized services (subgraphs) that each own a portion of your schema. A federation gateway sits in front of these services, presenting clients with a unified schema that spans all subgraphs.
The magic happens through entity references and the @key directive. A subgraph can define a type with @key to indicate it's an "entity" that can be referenced from other subgraphs. Other services can then extend that entity with additional fields. For example, a Users service might define the User type, while a Reviews service extends User with a reviews field. The gateway handles the complexity of fetching data from multiple services and joining it together.
Federation uses a technique called query planning. When a query comes in, the gateway analyzes which subgraphs need to be called and in what order, then executes the plan efficiently. It can parallelize requests when possible and uses entity representations to fetch related data. Each subgraph implements a special _entities query that allows the gateway to fetch specific entities by their keys.
Federation 2, the current version, introduces improved composition with better error handling, cleaner syntax (@shareable, @inaccessible, @override), and progressive @override for gradual migrations between services. It enables true microservices architecture for GraphQL while maintaining the single-graph experience that makes GraphQL powerful.
Code Example:
# Users Subgraph (service 1)
type User @key(fields: "id") {
id: ID!
email: String!
displayName: String!
}
type Query {
user(id: ID!): User
users: [User!]!
}
# Reviews Subgraph (service 2)
# Extends User type from Users subgraph
extend type User @key(fields: "id") {
id: ID! @external
reviews: [Review!]!
}
type Review {
id: ID!
rating: Int!
comment: String
author: User!
}
type Query {
reviews(userId: ID!): [Review!]!
}
# Products Subgraph (service 3)
type Product @key(fields: "id") {
id: ID!
name: String!
price: Float!
reviews: [Review!]!
}
# Extend Review to add product relationship
extend type Review @key(fields: "id") {
id: ID! @external
product: Product!
}
# Gateway composes these into unified schema:
# type User {
# id: ID!
# email: String!
# displayName: String!
# reviews: [Review!]! # From Reviews subgraph
# }
#
# type Review {
# id: ID!
# rating: Int!
# comment: String
# author: User!
# product: Product! # From Products subgraph
# }
Implementation Example:
// Users Subgraph
const { buildSubgraphSchema } = require('@apollo/subgraph');
const { gql } = require('apollo-server');
const typeDefs = gql`
type User @key(fields: "id") {
id: ID!
email: String!
displayName: String!
}
type Query {
user(id: ID!): User
}
`;
const resolvers = {
Query: {
user: (_, { id }) => getUserById(id)
},
User: {
__resolveReference: (reference) => {
// Called by gateway to resolve User entity
return getUserById(reference.id);
}
}
};
const schema = buildSubgraphSchema({ typeDefs, resolvers });
// Gateway
const { ApolloGateway } = require('@apollo/gateway');
const { ApolloServer } = require('apollo-server');
const gateway = new ApolloGateway({
supergraphSdl: /* composed schema */,
// Or fetch from Apollo Studio
});
const server = new ApolloServer({
gateway,
subscriptions: false
});
Apollo Federation Architecture:
flowchart TD
Client[GraphQL Client] --> Gateway[Apollo Gateway]
Gateway --> |Query Planning| Gateway
Gateway --> |Route & Execute| Subgraph1[Users Subgraph]
Gateway --> |Route & Execute| Subgraph2[Reviews Subgraph]
Gateway --> |Route & Execute| Subgraph3[Products Subgraph]
Subgraph1 --> DB1[(Users DB)]
Subgraph2 --> DB2[(Reviews DB)]
Subgraph3 --> DB3[(Products DB)]
Gateway --> |Compose Results| Response[Unified Response]
Response --> Client
Studio[Apollo Studio] -.-> |Schema Registry| Gateway
Subgraph1 -.-> |Publish Schema| Studio
Subgraph2 -.-> |Publish Schema| Studio
Subgraph3 -.-> |Publish Schema| Studio
style Gateway fill:#3f20ba
style Client fill:#9cf
style Response fill:#9cf
Federation Query Flow:
sequenceDiagram
participant Client
participant Gateway
participant Users
participant Reviews
Client->>Gateway: query { user(id: "1") { name, reviews { rating } } }
Gateway->>Gateway: Plan query execution
Gateway->>Users: query { user(id: "1") { id, name } }
Users-->>Gateway: { id: "1", name: "John" }
Gateway->>Reviews: query { _entities(representations: [{__typename: "User", id: "1"}]) { reviews { rating } } }
Reviews-->>Gateway: { reviews: [{ rating: 5 }] }
Gateway->>Gateway: Merge results
Gateway-->>Client: { user: { name: "John", reviews: [{ rating: 5 }] } }
References:
↑ Back to topPerformance and Optimization
What is persisted queries and how do they improve performance?
The 30-Second Answer: Persisted queries allow clients to send a hash/ID instead of the full query string, reducing request payload size and enabling aggressive caching at the CDN level. The server stores pre-registered query strings mapped to IDs, validating and executing them on demand. This improves performance through reduced bandwidth, faster parsing, enables query allow-listing for security, and makes CDN caching of POST requests possible.
The 2-Minute Answer (If They Want More): There are two main types of persisted queries: Automatic Persisted Queries (APQ) and Pre-registered Persisted Queries. APQ uses a hash of the query string as the identifier - on first request, the client sends the hash and full query; the server caches this mapping, and subsequent requests only need the hash. This reduces bandwidth and enables caching without pre-registration.
Pre-registered persisted queries require the query strings to be registered with the server during deployment. Clients reference queries by ID only, never sending the actual query text in production. This provides stronger security through query allow-listing (only registered queries can execute), prevents query injection attacks, and allows for query versioning and analytics.
Performance benefits are substantial: request sizes drop from several KB to just a few bytes (a hash/ID), CDN caching becomes possible since requests use GET instead of POST, GraphQL parsing and validation can be cached server-side, and network transfer time decreases significantly for mobile clients on slow connections.
The trade-off is increased deployment complexity - query registration must be coordinated between frontend and backend releases. Build tools extract queries from client code, generate IDs, and register them with the server. Apollo Client and Relay both have built-in support with different implementation approaches.
Code Example:
// Apollo Server with Automatic Persisted Queries (APQ)
const { ApolloServer } = require('apollo-server');
const { ApolloServerPluginCacheControl } = require('apollo-server-core');
const server = new ApolloServer({
typeDefs,
resolvers,
plugins: [
ApolloServerPluginCacheControl({ defaultMaxAge: 5 }),
],
persistedQueries: {
cache: {
// Use Redis for production
async get(key) {
return await redisClient.get(key);
},
async set(key, value) {
await redisClient.set(key, value, 'EX', 86400); // 24h TTL
},
},
},
});
// Client-side APQ with Apollo Client
import { ApolloClient, InMemoryCache, HttpLink } from '@apollo/client';
import { createPersistedQueryLink } from '@apollo/client/link/persisted-queries';
import { sha256 } from 'crypto-hash';
const httpLink = new HttpLink({ uri: '/graphql' });
const persistedQueryLink = createPersistedQueryLink({
sha256,
useGETForHashedQueries: true, // Enable CDN caching
});
const client = new ApolloClient({
cache: new InMemoryCache(),
link: persistedQueryLink.concat(httpLink),
});
// Pre-registered Persisted Queries approach
const persistedQueries = {
'getUserById': `
query GetUserById($id: ID!) {
user(id: $id) {
id
name
email
}
}
`,
'getUserPosts': `
query GetUserPosts($userId: ID!, $limit: Int) {
user(id: $userId) {
posts(limit: $limit) {
id
title
createdAt
}
}
}
`,
};
const { ApolloServer } = require('apollo-server-express');
const express = require('express');
const app = express();
// Custom middleware for persisted queries
app.use('/graphql', (req, res, next) => {
const { queryId, variables } = req.body;
if (queryId) {
const query = persistedQueries[queryId];
if (!query) {
return res.status(400).json({
errors: [{ message: 'Invalid query ID' }],
});
}
req.body.query = query;
}
next();
});
const server = new ApolloServer({
typeDefs,
resolvers,
});
server.applyMiddleware({ app });
// Client usage with pre-registered queries
fetch('/graphql', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
queryId: 'getUserById',
variables: { id: '123' },
}),
});
// Build-time query extraction example (using graphql-tag)
// queries.js
import { gql } from 'graphql-tag';
export const GET_USER = gql`
query GetUserById($id: ID!) {
user(id: $id) {
id
name
email
}
}
`;
// Build script extracts queries and generates manifest
// manifest.json
/*
{
"GET_USER": "abc123hash...",
"GET_POSTS": "def456hash..."
}
*/
References:
↑ Back to topGraphQL Fundamentals
What is GraphQL and what problems does it solve?
The 30-Second Answer: GraphQL is a query language and runtime for APIs developed by Facebook that allows clients to request exactly the data they need in a single request. It solves problems like over-fetching, under-fetching, and the need for multiple API endpoints by enabling clients to specify their data requirements declaratively.
The 2-Minute Answer (If They Want More): GraphQL was created by Facebook in 2012 and open-sourced in 2015 to address the limitations of traditional REST APIs in mobile and web applications. At its core, GraphQL is both a query language for APIs and a server-side runtime for executing those queries using a type system you define for your data.
The primary problems GraphQL solves include over-fetching (receiving more data than needed), under-fetching (requiring multiple requests to gather all needed data), and API versioning challenges. With REST APIs, clients are often forced to make multiple requests to different endpoints to gather related data, or they receive large payloads with unnecessary fields. GraphQL allows clients to request exactly what they need in a single query, improving performance and reducing bandwidth usage.
GraphQL provides a complete and understandable description of the data in your API through its strongly-typed schema, which serves as a contract between client and server. This schema-first approach enables better tooling, auto-completion, validation, and documentation generation. Additionally, GraphQL's introspection capabilities allow clients to discover what queries are available, making APIs self-documenting.
The declarative nature of GraphQL queries also enables better developer experience, as frontend developers can specify their data requirements without depending on backend changes for every new view or feature.
Code Example:
# REST approach might require multiple endpoints:
# GET /users/123
# GET /users/123/posts
# GET /posts/456/comments
# GraphQL accomplishes this in a single query:
query GetUserWithPosts {
user(id: "123") {
id
name
email
posts {
id
title
comments {
id
text
author {
name
}
}
}
}
}
Mermaid Diagram:
flowchart LR
A[Client] -->|Single Query| B[GraphQL API]
B -->|Exact Data| A
C[Client] -->|Request 1| D[REST API]
C -->|Request 2| D
C -->|Request 3| D
D -->|Over-fetched Data| C
References:
↑ Back to topMutations
What is a GraphQL mutation?
The 30-Second Answer: A GraphQL mutation is an operation that modifies data on the server, such as creating, updating, or deleting records. While queries are for reading data, mutations are for writing data and typically return the modified data to confirm the operation succeeded.
The 2-Minute Answer (If They Want More): Mutations are one of the three root operation types in GraphQL (alongside queries and subscriptions). They provide a standardized way to modify server-side data while maintaining GraphQL's type-safe, self-documenting API characteristics.
Like queries, mutations are defined in the schema with specific input parameters and return types. However, unlike queries which can be executed in parallel, mutations are executed sequentially by default to prevent race conditions. This is crucial when multiple mutations are sent in a single request that might depend on each other.
Mutations follow a request-response pattern where the client specifies what data to modify and what fields to return. This allows clients to verify the operation's success and immediately receive the updated data without making a separate query. The mutation's return type often includes the modified object, error information, and sometimes additional metadata about the operation.
In practice, mutations are the primary way GraphQL APIs handle all write operations—from simple user registration to complex multi-step transactions. They provide clear semantics about what actions are available and ensure that data modifications go through proper validation and authorization layers.
Code Example:
# Schema definition for a mutation
type Mutation {
createUser(input: CreateUserInput!): CreateUserPayload!
updateUser(id: ID!, input: UpdateUserInput!): UpdateUserPayload!
deleteUser(id: ID!): DeleteUserPayload!
}
input CreateUserInput {
name: String!
email: String!
age: Int
}
type CreateUserPayload {
user: User
errors: [String!]
}
type User {
id: ID!
name: String!
email: String!
age: Int
}
# Client mutation request
mutation CreateNewUser {
createUser(input: {
name: "Alice Smith"
email: "alice@example.com"
age: 28
}) {
user {
id
name
email
}
errors
}
}
# Server response
{
"data": {
"createUser": {
"user": {
"id": "123",
"name": "Alice Smith",
"email": "alice@example.com"
},
"errors": null
}
}
}
References:
↑ Back to topResolvers
What is a resolver in GraphQL?
The 30-Second Answer: A resolver is a function that's responsible for populating the data for a single field in your GraphQL schema. When a GraphQL query is executed, each field in the query calls its corresponding resolver function to fetch or compute its value, forming a chain of function calls that builds the complete response.
The 2-Minute Answer (If They Want More): Resolvers are the fundamental building blocks that connect your GraphQL schema to your actual data sources. They act as the bridge between the declarative GraphQL query language and your imperative data-fetching logic. Each field in your schema can have its own resolver function, though GraphQL provides default resolvers for simple property access.
The resolver execution process is hierarchical and follows the structure of the query. GraphQL starts at the root query fields and recursively calls resolvers for nested fields, building up the response object field by field. This design makes GraphQL extremely flexible - different fields can fetch data from different sources (databases, REST APIs, microservices, etc.) and the client doesn't need to know or care about these implementation details.
Resolvers are where you implement business logic, handle authentication and authorization, orchestrate calls to multiple data sources, transform data formats, and handle errors. They're also where you can implement performance optimizations like batching and caching using tools like DataLoader. This separation of concerns - schema definition separate from data fetching - is one of GraphQL's key architectural advantages.
The resolver map typically mirrors your schema structure, making it easy to understand how each field gets its data. This predictable pattern makes GraphQL APIs easier to maintain and reason about compared to traditional REST endpoints where data-fetching logic is often scattered across multiple route handlers.
Code Example:
// Basic resolver structure
const resolvers = {
// Query root resolvers
Query: {
user: (parent, args, context, info) => {
return context.db.getUserById(args.id);
},
users: (parent, args, context, info) => {
return context.db.getAllUsers();
}
},
// Type resolvers - resolve fields on User type
User: {
// Explicit resolver for computed field
fullName: (parent, args, context, info) => {
return `${parent.firstName} ${parent.lastName}`;
},
// Resolver for relationship field
posts: (parent, args, context, info) => {
return context.db.getPostsByUserId(parent.id);
}
// Fields like 'id', 'firstName', 'lastName' use default resolvers
// that simply return parent[fieldName]
},
// Mutation resolvers
Mutation: {
createUser: (parent, args, context, context) => {
return context.db.createUser(args.input);
}
}
};
// Example schema this resolves
const typeDefs = `
type Query {
user(id: ID!): User
users: [User!]!
}
type User {
id: ID!
firstName: String!
lastName: String!
fullName: String!
posts: [Post!]!
}
type Mutation {
createUser(input: CreateUserInput!): User!
}
`;
Mermaid Diagram:
flowchart TD
A[GraphQL Query] --> B[Root Resolver]
B --> C[user Resolver]
C --> D[User Object]
D --> E[firstName Resolver]
D --> F[lastName Resolver]
D --> G[fullName Resolver]
D --> H[posts Resolver]
H --> I[Array of Post Objects]
I --> J[Post Field Resolvers]
style A fill:#e1f5ff
style B fill:#fff4e1
style C fill:#fff4e1
style G fill:#ffe1e1
style H fill:#ffe1e1
References:
↑ Back to topQueries
What is a GraphQL query?
The 30-Second Answer: A GraphQL query is a read operation that requests specific data from a GraphQL server. Unlike REST APIs where you get all fields from an endpoint, a query lets you specify exactly which fields you need, reducing over-fetching and under-fetching of data.
The 2-Minute Answer (If They Want More):
A GraphQL query is the fundamental way to fetch data in GraphQL. It's a declarative description of the data you want, structured in a JSON-like syntax that mirrors the shape of the response you'll receive. Queries are sent to a single GraphQL endpoint (typically /graphql) via HTTP POST requests.
The key advantage of queries is that they solve the over-fetching and under-fetching problems common in REST APIs. You request only the fields you need, nothing more, nothing less. This is particularly valuable for mobile applications with bandwidth constraints or complex UIs that need data from multiple sources.
Queries are strongly typed based on your GraphQL schema, which means you get excellent tooling support including autocomplete, validation, and documentation. The GraphQL server validates your query against the schema before executing it, catching errors early in the development process.
Every query starts at the root Query type and traverses the schema graph to fetch the requested data. The server resolves each field independently, which allows for efficient caching and optimization strategies.
Code Example:
# Simple query to fetch a user's name and email
query GetUser {
user(id: "123") {
name
email
}
}
# Response shape matches the query structure
{
"data": {
"user": {
"name": "John Doe",
"email": "john@example.com"
}
}
}
References:
- GraphQL Queries and Mutations - Official Documentation
- Introduction to GraphQL Queries - Apollo GraphQL
Subscriptions
What is a pub/sub system in the context of subscriptions?
The 30-Second Answer: A pub/sub (publish/subscribe) system is a messaging pattern where publishers emit events without knowing who will receive them, and subscribers register interest in specific events without knowing who publishes them. In GraphQL subscriptions, the pub/sub system acts as the intermediary that routes events from data sources (publishers) to active subscription connections (subscribers).
The 2-Minute Answer (If They Want More): Pub/sub systems provide the event distribution infrastructure that makes GraphQL subscriptions work. When a mutation creates new data or an external event occurs, the code publishes an event to a specific topic or channel in the pub/sub system. Meanwhile, active GraphQL subscription connections have registered as subscribers to relevant topics. The pub/sub system handles the complexity of maintaining the list of subscribers and delivering events to all of them.
This decoupling is crucial for scalability and flexibility. The code that creates data (publisher) doesn't need to know about active subscriptions or how to deliver updates—it just publishes to the pub/sub system. The GraphQL subscription resolver (subscriber) doesn't need to know where events originate—it just registers interest and receives notifications. This separation allows you to have multiple publishers (different mutations, background jobs, external webhooks) and multiple subscribers (different subscription queries, multiple instances of the same subscription) without tight coupling.
In simple applications, you might use an in-memory pub/sub implementation like Node.js EventEmitter or the PubSub class from graphql-subscriptions. However, in-memory solutions only work for single-server deployments. For production systems with multiple server instances, you need a distributed pub/sub system like Redis Pub/Sub, Google Cloud Pub/Sub, AWS SNS/SQS, or message queues like RabbitMQ or Apache Kafka. These systems ensure that when any server instance publishes an event, all server instances with subscribers receive it.
The pub/sub pattern also enables advanced features like filtering (subscribers only receive events matching certain criteria), fan-out (one event delivered to many subscribers), and durability (events can be persisted and replayed).
Code Example:
# Schema
type Subscription {
messageAdded(channelId: ID!): Message!
}
type Mutation {
sendMessage(channelId: ID!, content: String!): Message!
}
# Server implementation (Node.js with Apollo Server)
const { PubSub } = require('graphql-subscriptions');
const { RedisPubSub } = require('graphql-redis-subscriptions');
// In-memory pub/sub (development only - single server)
// const pubsub = new PubSub();
// Redis pub/sub (production - distributed)
const pubsub = new RedisPubSub({
connection: {
host: 'localhost',
port: 6379
}
});
// Event channel names
const MESSAGE_ADDED = 'MESSAGE_ADDED';
const resolvers = {
Mutation: {
sendMessage: async (_, { channelId, content }, { db, userId }) => {
// Save message to database
const message = await db.createMessage({
channelId,
content,
authorId: userId
});
// PUBLISH: Emit event to pub/sub system
// All subscribers to MESSAGE_ADDED will receive this
pubsub.publish(MESSAGE_ADDED, {
messageAdded: message,
channelId // Include for filtering
});
return message;
}
},
Subscription: {
messageAdded: {
// SUBSCRIBE: Register interest in MESSAGE_ADDED events
subscribe: (_, { channelId }) => {
// Return async iterator that yields events
return pubsub.asyncIterator([MESSAGE_ADDED]);
},
// FILTER: Only send events matching this channel
filter: (payload, variables) => {
return payload.channelId === variables.channelId;
}
}
}
};
// Different pub/sub implementations for different scales:
// 1. In-memory (single server, development)
const memoryPubSub = new PubSub();
// 2. Redis (multiple servers, moderate scale)
const redisPubSub = new RedisPubSub({
connection: process.env.REDIS_URL
});
// 3. Google Cloud (enterprise scale)
const { GooglePubSub } = require('@axelspringer/graphql-google-pubsub');
const googlePubSub = new GooglePubSub({
projectId: 'my-project'
});
// 4. Kafka (high-throughput, event streaming)
const { KafkaPubSub } = require('graphql-kafka-subscriptions');
const kafkaPubSub = new KafkaPubSub({
topic: 'graphql-events',
host: 'kafka-broker',
port: '9092'
});
Mermaid Diagram:
flowchart TD
subgraph Publishers
M1[Mutation Resolver]
M2[Background Job]
M3[External Webhook]
end
subgraph PubSub["Pub/Sub System (Redis, Kafka, etc.)"]
T1[Topic: MESSAGE_ADDED]
T2[Topic: USER_UPDATED]
T3[Topic: NOTIFICATION_SENT]
end
subgraph Subscribers
S1[Subscription 1<br/>Client A]
S2[Subscription 2<br/>Client B]
S3[Subscription 3<br/>Client C]
S4[Subscription 4<br/>Client D]
end
M1 -->|publish event| T1
M2 -->|publish event| T2
M3 -->|publish event| T1
M3 -->|publish event| T3
T1 -->|notify| S1
T1 -->|notify| S2
T2 -->|notify| S3
T3 -->|notify| S4
style PubSub fill:#FFE4B5
style Publishers fill:#90EE90
style Subscribers fill:#87CEEB
References:
↑ Back to topData Fetching and N+1 Problem
What is the N+1 problem in GraphQL?
The 30-Second Answer: The N+1 problem occurs when a GraphQL resolver makes 1 database query to fetch a list of N items, then makes N additional queries to fetch related data for each item. This results in N+1 total queries instead of 2 optimized queries, causing severe performance degradation.
The 2-Minute Answer (If They Want More): The N+1 problem is a classic database performance issue that becomes particularly visible in GraphQL due to its nested query structure. When you query for a list of items and their relationships, naive resolver implementations will execute one query per parent item to fetch the related data.
For example, if you query for 100 users and their posts, a naive implementation would execute 1 query to get all users, then 100 separate queries (one per user) to get each user's posts. This exponentially increases database load and response time, especially with deeply nested queries.
The problem is exacerbated in GraphQL because clients can freely compose nested queries, and each resolver function is called independently. Without proper optimization, each resolver makes its own database call, unaware that other resolvers might be requesting similar data.
This issue affects both REST and GraphQL APIs, but GraphQL's flexible query structure makes it more prevalent and harder to detect without proper monitoring and tooling.
Code Example:
// BAD: N+1 problem - makes 101 queries for 100 users
const resolvers = {
Query: {
users: () => db.getAllUsers() // 1 query
},
User: {
posts: (user) => db.getPostsByUserId(user.id) // N queries (100 queries)
}
};
// Query that triggers N+1
const query = gql`
query {
users {
id
name
posts {
id
title
}
}
}
`;
// Result: 1 + 100 = 101 database queries
Mermaid Diagram:
flowchart TD
A[GraphQL Query: users + posts] --> B[Resolver: getAllUsers]
B --> C[DB Query 1: SELECT * FROM users]
C --> D[Returns 100 users]
D --> E[Resolver: posts for User 1]
D --> F[Resolver: posts for User 2]
D --> G[Resolver: posts for User 3]
D --> H[Resolver: posts for User 100]
E --> I[DB Query 2: SELECT * FROM posts WHERE user_id=1]
F --> J[DB Query 3: SELECT * FROM posts WHERE user_id=2]
G --> K[DB Query 4: SELECT * FROM posts WHERE user_id=3]
H --> L[DB Query 101: SELECT * FROM posts WHERE user_id=100]
style C fill:#ff6b6b
style I fill:#ff6b6b
style J fill:#ff6b6b
style K fill:#ff6b6b
style L fill:#ff6b6b
References:
↑ Back to topError Handling
How does error handling work in GraphQL?
The 30-Second Answer:
GraphQL uses a structured error handling approach where errors are returned in a dedicated errors array alongside any successful data. Unlike REST APIs that use HTTP status codes, GraphQL requests typically return 200 OK even with errors, and the actual error information is embedded in the response body's errors field.
The 2-Minute Answer (If They Want More):
GraphQL's error handling model is fundamentally different from traditional REST APIs. Every GraphQL response follows a consistent structure with optional data and errors fields at the root level. When errors occur during query execution, they don't necessarily halt the entire operation—GraphQL can return partial data for fields that resolved successfully while including error details for fields that failed.
Errors in GraphQL can occur at different stages: validation errors (before execution), resolver errors (during execution), and network errors (transport level). The GraphQL specification defines a standard error format that includes a message, optional locations pointing to the query position, a path array indicating which field caused the error, and optional extensions for additional context like error codes or stack traces.
This approach allows clients to gracefully handle failures by checking for both data and errors in every response. For example, if fetching a user's profile succeeds but fetching their recent posts fails, the client still receives the profile data and can display it while handling the posts error appropriately. This partial success model is particularly valuable in complex queries with multiple independent data sources.
The server-side implementation typically involves throwing errors in resolvers, which GraphQL catches and formats according to the specification. Developers can extend the base error class to create custom error types with additional metadata, enabling fine-grained error handling on the client side.
Code Example:
// GraphQL Error Response Structure
{
"data": {
"user": {
"id": "123",
"name": "John Doe",
"posts": null // Failed to resolve
}
},
"errors": [
{
"message": "Failed to fetch user posts",
"locations": [{ "line": 4, "column": 5 }],
"path": ["user", "posts"],
"extensions": {
"code": "DATABASE_ERROR",
"timestamp": "2025-12-22T10:30:00Z"
}
}
]
}
// Server-side Resolver with Error Handling
const resolvers = {
Query: {
user: async (parent, { id }, context) => {
try {
const user = await context.db.getUserById(id);
if (!user) {
throw new GraphQLError('User not found', {
extensions: {
code: 'USER_NOT_FOUND',
userId: id
}
});
}
return user;
} catch (error) {
// Log error for monitoring
console.error('User fetch error:', error);
throw error;
}
}
},
User: {
posts: async (user, args, context) => {
try {
return await context.db.getPostsByUser(user.id);
} catch (error) {
// This error will be caught and formatted by GraphQL
throw new GraphQLError('Failed to fetch user posts', {
extensions: {
code: 'DATABASE_ERROR',
originalError: error.message
}
});
}
}
}
};
// Client-side Error Handling
async function fetchUserData(userId) {
const response = await fetch('/graphql', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
query: `
query GetUser($id: ID!) {
user(id: $id) {
id
name
email
posts { id title }
}
}
`,
variables: { id: userId }
})
});
const result = await response.json();
// Check for errors
if (result.errors) {
result.errors.forEach(error => {
console.error(`GraphQL Error: ${error.message}`, error);
// Handle specific error codes
if (error.extensions?.code === 'USER_NOT_FOUND') {
showNotification('User not found');
} else if (error.extensions?.code === 'DATABASE_ERROR') {
showNotification('Service temporarily unavailable');
}
});
}
// Use partial data if available
if (result.data?.user) {
return result.data.user;
}
return null;
}
References:
↑ Back to topPagination
What is cursor-based pagination?
The 30-Second Answer: Cursor-based pagination uses opaque string identifiers (cursors) to mark specific positions in a dataset. Clients request items before or after a cursor, making pagination stable even when data changes. Each item in the result set includes its cursor, which can be used to fetch subsequent pages.
The 2-Minute Answer (If They Want More): A cursor is an opaque string that uniquely identifies a position in a dataset. Unlike offset-based pagination where you specify "skip 20 items," cursor-based pagination says "give me items after this specific cursor." The cursor is typically a base64-encoded value containing information like the item's ID, timestamp, or position.
The key advantage is stability: if items are added or deleted between requests, cursor-based pagination maintains consistency. For example, if you fetch items 1-10 and someone deletes item 5, requesting "after cursor of item 10" still gives you the correct next set without duplicates or gaps.
Cursors are intentionally opaque to prevent clients from manipulating them or making assumptions about the underlying data structure. The server can change the cursor implementation (from ID-based to timestamp-based, for example) without breaking clients.
Cursor-based pagination typically uses first/after for forward pagination and last/before for backward pagination. The first parameter specifies how many items to fetch, while after specifies the cursor to start from. This bidirectional capability makes it ideal for infinite scrolling and complex navigation patterns.
Code Example:
# Schema definition
type Query {
posts(first: Int, after: String, last: Int, before: String): PostConnection!
}
type PostConnection {
edges: [PostEdge!]!
pageInfo: PageInfo!
}
type PostEdge {
cursor: String!
node: Post!
}
type Post {
id: ID!
title: String!
content: String!
}
# Forward pagination - fetch first 10 posts
query {
posts(first: 10) {
edges {
cursor
node {
id
title
}
}
pageInfo {
hasNextPage
endCursor
}
}
}
# Fetch next page using endCursor from previous query
query {
posts(first: 10, after: "Y3Vyc29yOjEw") {
edges {
cursor
node {
id
title
}
}
pageInfo {
hasNextPage
endCursor
}
}
}
# Backward pagination - fetch last 10 posts before a cursor
query {
posts(last: 10, before: "Y3Vyc29yOjUw") {
edges {
cursor
node {
id
title
}
}
pageInfo {
hasPreviousPage
startCursor
}
}
}
Mermaid Diagram:
sequenceDiagram
participant Client
participant Server
participant Database
Client->>Server: Query posts(first: 10)
Server->>Database: Fetch 10 posts
Database-->>Server: Posts 1-10
Server-->>Client: Edges with cursors + endCursor
Note over Client: User scrolls, needs more data
Client->>Server: Query posts(first: 10, after: endCursor)
Server->>Database: Fetch 10 posts after cursor
Database-->>Server: Posts 11-20
Server-->>Client: Edges with cursors + endCursor
Note over Client: Stable pagination even if<br/>data changes between requests
References:
↑ Back to topSecurity
What are the common security vulnerabilities in GraphQL?
The 30-Second Answer: GraphQL's main security vulnerabilities include excessive query depth/complexity attacks, introspection exposure in production, insufficient authentication/authorization, injection attacks through user inputs, and N+1 query performance issues that can be exploited for DoS. Unlike REST, GraphQL's flexible query structure makes rate limiting and query cost analysis critical.
The 2-Minute Answer (If They Want More): GraphQL introduces unique security challenges due to its flexible nature. The most critical vulnerability is query complexity attacks - malicious actors can craft deeply nested queries or request massive amounts of data in a single request, potentially crashing your server. For example, a query requesting users, their posts, comments on those posts, and authors of those comments can quickly spiral out of control.
Introspection, while useful in development, exposes your entire schema in production, giving attackers a complete map of your API. Authorization is another major concern - unlike REST where endpoints have clear boundaries, GraphQL requires field-level authorization checks to prevent users from accessing data they shouldn't see through creative queries.
Injection attacks remain relevant in GraphQL, particularly when queries interact with databases or external services. Batching attacks can bypass traditional rate limiting by bundling multiple operations in a single request. Additionally, inadequate input validation can lead to data corruption or unauthorized operations.
The open-ended nature of GraphQL means traditional REST security patterns don't translate directly - you need specialized tools like query depth limiting, cost analysis, persisted queries, and comprehensive authorization at the resolver level.
Code Example:
// Common vulnerabilities example
// VULNERABLE: No depth or complexity limits
const server = new ApolloServer({
typeDefs,
resolvers,
// This allows dangerous queries!
});
// Malicious query example - can crash server
const maliciousQuery = `
query {
users {
posts {
comments {
author {
posts {
comments {
author {
posts {
# ... infinitely nested
}
}
}
}
}
}
}
}
}
`;
// SECURED: With proper protections
import depthLimit from 'graphql-depth-limit';
import { createComplexityLimitRule } from 'graphql-validation-complexity';
const server = new ApolloServer({
typeDefs,
resolvers,
validationRules: [
depthLimit(5), // Limit query depth
createComplexityLimitRule(1000), // Limit complexity score
],
introspection: process.env.NODE_ENV !== 'production',
});
References:
↑ Back to top