All topics
General · Learning hub

GraphQL notes for developers

Master GraphQL with a curated set of 3 developer notes — core concepts, patterns, and interview prep. Maintained by the DevRecall team.

Save this stack to your DevRecallMore General notes
GraphQL

Schema & Types

Schema & Types Type System # Scalar types String Int Float Boolean ID # Custom scalars scalar DateTime scalar JSON scalar Upload # Object type type User { id: I

Schema & Types

Type System

# Scalar types
String  Int  Float  Boolean  ID

# Custom scalars
scalar DateTime
scalar JSON
scalar Upload

# Object type
type User {
  id: ID!              # ! = non-nullable
  email: String!
  name: String!
  bio: String          # nullable
  age: Int
  role: Role!
  posts: [Post!]!      # non-nullable list of non-nullable Posts
  createdAt: DateTime!
}

# Enum
enum Role {
  USER
  ADMIN
  MODERATOR
}

# Interface
interface Node {
  id: ID!
}

type User implements Node {
  id: ID!
  email: String!
}

# Union
union SearchResult = User | Post | Comment

# Input type (for mutations/arguments)
input CreateUserInput {
  email: String!
  name: String!
  password: String!
  bio: String
}

input PaginationInput {
  page: Int = 1
  limit: Int = 10
}

Query & Mutation Types

# Root types
type Query {
  user(id: ID!): User
  users(pagination: PaginationInput, role: Role): [User!]!
  me: User
  searchUsers(query: String!): [User!]!
}

type Mutation {
  createUser(input: CreateUserInput!): User!
  updateUser(id: ID!, input: UpdateUserInput!): User!
  deleteUser(id: ID!): Boolean!
  login(email: String!, password: String!): AuthPayload!
}

type AuthPayload {
  token: String!
  user: User!
}

type Subscription {
  userCreated: User!
  messageAdded(channelId: ID!): Message!
}

# Field arguments
type Post {
  id: ID!
  title: String!
  comments(limit: Int = 10, after: ID): CommentConnection!
}

# Pagination — Relay-style cursor connection
type UserConnection {
  edges: [UserEdge!]!
  pageInfo: PageInfo!
  totalCount: Int!
}

type UserEdge {
  cursor: String!
  node: User!
}

type PageInfo {
  hasNextPage: Boolean!
  hasPreviousPage: Boolean!
  startCursor: String
  endCursor: String
}

Directives

# Built-in directives
query GetUser($withPosts: Boolean!) {
  user(id: "1") {
    name
    posts @include(if: $withPosts) {
      title
    }
    bio @skip(if: true)
  }
}

# Custom directives (server-side)
directive @auth(requires: Role = USER) on FIELD_DEFINITION
directive @deprecated(reason: String = "No longer supported") on FIELD_DEFINITION | ENUM_VALUE
directive @upper on FIELD_DEFINITION

type User {
  email: String! @auth(requires: ADMIN)
  oldField: String @deprecated(reason: "Use newField instead")
}

Schema Stitching / Federation

# Apollo Federation subgraph (service-specific schema)
extend schema @link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@key"])

type User @key(fields: "id") {
  id: ID!
  email: String!
  name: String!
}

# Another subgraph can extend User
type User @key(fields: "id") {
  id: ID! @external
  orders: [Order!]!
}
GraphQL

Queries, Mutations & Apollo Client

Queries, Mutations & Apollo Client Writing Queries # Basic query query GetUser { user(id: "1") { id name email } } # With variables query GetUser($id: ID!) { us

Queries, Mutations & Apollo Client

Writing Queries

# Basic query
query GetUser {
  user(id: "1") {
    id
    name
    email
  }
}

# With variables
query GetUser($id: ID!) {
  user(id: $id) {
    id
    name
    email
    posts(limit: 5) {
      id
      title
    }
  }
}

# Fragments — reusable field sets
fragment UserFields on User {
  id
  name
  email
  createdAt
}

query GetUsers {
  users {
    ...UserFields
    posts { title }
  }
}

# Aliases — rename fields in response
query CompareUsers {
  alice: user(id: "1") { name email }
  bob: user(id: "2") { name email }
}

# Inline fragments — for interfaces/unions
query Search($q: String!) {
  search(query: $q) {
    ... on User { id name email }
    ... on Post { id title author { name } }
    ... on Comment { id body }
  }
}

# Mutation
mutation CreateUser($input: CreateUserInput!) {
  createUser(input: $input) {
    id
    name
    email
  }
}

# Subscription
subscription OnMessageAdded($channelId: ID!) {
  messageAdded(channelId: $channelId) {
    id
    body
    author { name }
  }
}

Apollo Client (React)

import { gql, useQuery, useMutation, useLazyQuery } from '@apollo/client';

// Define queries
const GET_USER = gql`
  query GetUser($id: ID!) {
    user(id: $id) {
      id
      name
      email
    }
  }
`;

const CREATE_USER = gql`
  mutation CreateUser($input: CreateUserInput!) {
    createUser(input: $input) {
      id
      name
    }
  }
`;

// useQuery — fetches on mount
function UserProfile({ id }: { id: string }) {
  const { data, loading, error, refetch } = useQuery(GET_USER, {
    variables: { id },
    fetchPolicy: 'cache-first',   // cache-first | network-only | cache-and-network
    skip: !id,                    // skip if no id
    pollInterval: 30000,          // poll every 30s
  });

  if (loading) return <Spinner />;
  if (error) return <Error message={error.message} />;
  return <div>{data.user.name}</div>;
}

// useLazyQuery — fetches on demand
const [searchUsers, { data, loading }] = useLazyQuery(SEARCH_USERS, {
  fetchPolicy: 'network-only',
});
// Call when needed:
searchUsers({ variables: { query: 'alice' } });

// useMutation
const [createUser, { loading: creating }] = useMutation(CREATE_USER, {
  // Update cache after mutation
  update(cache, { data }) {
    cache.modify({
      fields: {
        users(existingUsers = []) {
          const newUserRef = cache.writeFragment({
            data: data.createUser,
            fragment: gql`fragment NewUser on User { id name }`,
          });
          return [...existingUsers, newUserRef];
        },
      },
    });
  },
  // Or simpler — refetch affected queries
  refetchQueries: [{ query: GET_USERS }],
  onCompleted: (data) => toast.success(`Created ${data.createUser.name}`),
  onError: (error) => toast.error(error.message),
});

await createUser({ variables: { input: { name: 'Alice', email: 'a@b.com' } } });

Apollo Client Setup

import { ApolloClient, InMemoryCache, createHttpLink, ApolloProvider, from } from '@apollo/client';
import { setContext } from '@apollo/client/link/context';
import { onError } from '@apollo/client/link/error';

const httpLink = createHttpLink({ uri: '/api/graphql' });

const authLink = setContext((_, { headers }) => ({
  headers: {
    ...headers,
    authorization: localStorage.getItem('token')
      ? `Bearer ${localStorage.getItem('token')}`
      : '',
  },
}));

const errorLink = onError(({ graphQLErrors, networkError }) => {
  if (graphQLErrors) {
    graphQLErrors.forEach(({ message, locations, path }) =>
      console.error(`[GraphQL error]: ${message}`)
    );
  }
  if (networkError) console.error(`[Network error]: ${networkError}`);
});

const client = new ApolloClient({
  link: from([errorLink, authLink, httpLink]),
  cache: new InMemoryCache({
    typePolicies: {
      User: { keyFields: ['id'] },
      Query: {
        fields: {
          users: { merge: false },  // replace array on refetch
        },
      },
    },
  }),
  defaultOptions: {
    watchQuery: { errorPolicy: 'all' },
  },
});

// Wrap app
<ApolloProvider client={client}>
  <App />
</ApolloProvider>
GraphQL

Interview Questions

GraphQL Interview Questions Q: What is GraphQL and how does it differ from REST? GraphQL is a query language and runtime for APIs. Key differences from REST: (1

GraphQL Interview Questions

Q: What is GraphQL and how does it differ from REST?

GraphQL is a query language and runtime for APIs. Key differences from REST: (1) Single endpoint vs multiple endpoints. (2) Client specifies exactly what data it needs — no over-fetching or under-fetching. (3) Strongly typed schema as the contract. (4) Multiple resources in one request (vs multiple REST calls). (5) Real-time with subscriptions. Trade-offs: more complex caching, file uploads are awkward, introspection can be a security risk.

Q: What is the N+1 problem in GraphQL and how do you solve it?

When resolving a list of N items, if each item's resolver makes a separate DB query for related data, you get N+1 queries. Solution: DataLoader — batch and cache per-request. DataLoader collects all IDs requested in a tick, makes one batched query, and distributes results back to each resolver.

import DataLoader from 'dataloader';

const userLoader = new DataLoader(async (ids: readonly string[]) => {
  const users = await db.users.findMany({ where: { id: { in: ids as string[] } } });
  return ids.map(id => users.find(u => u.id === id) ?? null);
});

// In resolver
posts: {
  author: (post) => userLoader.load(post.authorId), // batched automatically
}

Q: What is the difference between a query and a mutation?

Queries are read operations — they should have no side effects and can be executed in parallel. Mutations are write operations — they run serially (one at a time) in the order they appear. By convention, queries are idempotent (same result each call) and mutations change server state.

Q: What are fragments and why use them?

Fragments are reusable field selections defined once and spread into queries with ...FragmentName. They eliminate duplication when the same fields are needed in multiple queries, make queries more readable, and keep client-side field specifications consistent. Apollo Client co-locates fragments with the components that use them.

Q: How do you handle authentication in GraphQL?

Pass the token in the Authorization HTTP header (same as REST). In the GraphQL context function, validate the token and attach the user to context. Each resolver can then check context.user for authentication. For field-level auth, use schema directives (@auth) or check permissions inside resolvers. Never expose sensitive fields without auth checks.

Q: What is introspection and should you disable it in production?

Introspection allows clients to query the schema itself (what types/fields/queries exist). It's used by tools like GraphiQL and code generators. In production, disable introspection to avoid exposing your schema structure to attackers — this reduces the attack surface for query crafting. Allow it for authenticated internal clients if needed.

Keep your GraphQL knowledge sharp.

Save this stack to your personal DevRecall — add your own notes, track what you're learning, and share what you know with the community.

Get started — free forever