Toolmingo
Guides21 min read

50 GraphQL Interview Questions (With Answers)

Top GraphQL interview questions with clear answers and examples — covering schema design, resolvers, N+1 problem, DataLoader, authentication, subscriptions, federation, and Apollo.

GraphQL interviews test your understanding of schema-first design, the type system, resolver execution, performance pitfalls (especially the N+1 problem), and real-world patterns like authentication and pagination. This guide covers the 50 most common questions — with concise answers and code examples.

Quick reference

Topic Most asked questions
Fundamentals SDL, types, queries vs mutations vs subscriptions
Schema design Object types, input types, unions, interfaces, enums
Resolvers Resolver chain, context, args, parent
N+1 problem DataLoader, batching, caching
Authentication JWT in context, field-level auth, directives
Pagination Cursor-based vs offset, Relay connection spec
Error handling Partial success, error extensions, nullable vs non-null
Performance Persisted queries, query depth/complexity limits
Subscriptions WebSocket, server-sent events, real-time data
Federation Apollo Federation, subgraphs, supergraph

GraphQL fundamentals

1. What is GraphQL and how does it differ from REST?

GraphQL is a query language for APIs and a server-side runtime for executing those queries. Key differences from REST:

Dimension REST GraphQL
Data fetching Multiple endpoints, fixed response shape Single endpoint, client specifies shape
Over-fetching Common (extra fields returned) Eliminated (request only what you need)
Under-fetching Common (multiple requests needed) Eliminated (fetch related data in one query)
Versioning URL versioning (/v1, /v2) Schema evolution with deprecations
Type system OpenAPI/Swagger (optional) Built-in, mandatory SDL
Caching HTTP caching (ETags, Cache-Control) Requires custom cache keys (persisted queries)
Tooling Postman, cURL GraphiQL, Apollo Studio, Altair

GraphQL shines for complex, interconnected data and mobile clients where bandwidth matters. REST is simpler for CRUD-heavy APIs with predictable access patterns.

2. What is the GraphQL Schema Definition Language (SDL)?

SDL is the language used to describe a GraphQL schema — types, fields, and relationships.

type User {
  id: ID!
  name: String!
  email: String!
  posts: [Post!]!
  createdAt: String
}

type Post {
  id: ID!
  title: String!
  body: String!
  author: User!
  tags: [String!]!
}

type Query {
  user(id: ID!): User
  users: [User!]!
  post(id: ID!): Post
}

type Mutation {
  createPost(input: CreatePostInput!): Post!
  updatePost(id: ID!, input: UpdatePostInput!): Post
  deletePost(id: ID!): Boolean!
}

input CreatePostInput {
  title: String!
  body: String!
  tags: [String!]
}

The ! suffix means non-null. [Post!]! means a non-null list of non-null Posts.

3. What are the three root operation types in GraphQL?

Operation Purpose Analogous REST
Query Read data (idempotent) GET
Mutation Write/modify data POST, PUT, PATCH, DELETE
Subscription Real-time event stream WebSocket / SSE

Subscriptions maintain a long-lived connection and push updates when data changes.

4. What is the difference between a field and an argument in GraphQL?

Fields are the properties you can request on a type. Arguments are parameters passed to a field to filter, paginate, or transform the result.

# Field: name, email, posts
# Arguments: first, after (on the posts field)
query {
  user(id: "1") {        # "id" is an argument on the user field
    name
    email
    posts(first: 10, after: "cursor123") {  # arguments on posts field
      id
      title
    }
  }
}

5. What is an alias in GraphQL and when would you use it?

Aliases let you rename the result of a field, which is useful when fetching the same field with different arguments.

query {
  activeUser: user(id: "1") {
    name
    email
  }
  suspendedUser: user(id: "2") {
    name
    email
  }
}

Without aliases, two user fields would conflict in the response object.

6. What are fragments in GraphQL?

Fragments are reusable units of fields that can be spread across multiple queries — reducing duplication.

fragment UserBasic on User {
  id
  name
  email
}

query {
  user(id: "1") {
    ...UserBasic
    posts {
      id
      title
    }
  }
  anotherUser: user(id: "2") {
    ...UserBasic
  }
}

Inline fragments are used for conditional fields based on type:

query {
  search(term: "foo") {
    ... on User { name email }
    ... on Post { title body }
  }
}

7. What is an introspection query?

Introspection lets clients query the schema itself — discovering types, fields, and their descriptions. GraphiQL and Apollo Studio use introspection to build their explorer UIs.

query {
  __schema {
    types {
      name
      kind
      fields {
        name
        type { name kind }
      }
    }
  }
}

Security note: Disable introspection in production to prevent schema enumeration by attackers.


Schema design

8. What is the difference between type, interface, and union in GraphQL?

Construct Purpose Shared fields
type Concrete object type N/A
interface Abstract type with guaranteed fields Yes — implementors must include all interface fields
union Group of unrelated types No — members share nothing
# Interface — all implementors have title and publishedAt
interface Content {
  id: ID!
  title: String!
  publishedAt: String!
}

type Article implements Content {
  id: ID!
  title: String!
  publishedAt: String!
  wordCount: Int!
}

type Video implements Content {
  id: ID!
  title: String!
  publishedAt: String!
  durationSeconds: Int!
}

# Union — no shared fields required
union SearchResult = User | Post | Comment

Use interface when types share common fields; union when they are unrelated.

9. What are input types and why are they needed?

Input types (input) are used for mutation arguments. Regular object type cannot be used as argument types because they may contain fields with their own arguments or circular references.

# WRONG — cannot use regular type as argument
type Mutation {
  createUser(user: User): User  # Error!
}

# CORRECT — use input type
input CreateUserInput {
  name: String!
  email: String!
  role: UserRole = USER
}

type Mutation {
  createUser(input: CreateUserInput!): User!
}

10. What are GraphQL enums and scalars?

Enums restrict a field to a set of allowed values:

enum UserRole {
  ADMIN
  EDITOR
  VIEWER
}

Scalars are primitive leaf types. Built-in scalars: Int, Float, String, Boolean, ID. Custom scalars extend the type system:

scalar DateTime
scalar JSON
scalar Upload

Custom scalars need serialise/parse/parseLiteral implementations in the server.

11. What is the @deprecated directive?

Directives annotate schema elements. @deprecated marks a field as obsolete without removing it, allowing graceful schema evolution:

type User {
  id: ID!
  name: String!
  username: String @deprecated(reason: "Use `name` instead")
  email: String!
}

Other built-in directives: @include(if: Boolean) and @skip(if: Boolean) for conditional field fetching on the client.

12. How do you handle nullable vs non-null fields?

Design non-null (!) carefully:

Decision When
Non-null field field: Type! Data is always present — absence is a server error
Nullable field field: Type Data may legitimately not exist
Non-null list [Type!]! List always exists and has no null members

Principle: Prefer nullable for resilience. A non-null violation makes the entire parent object null, propagating up the response tree. If root fields are non-null and throw, the entire data object becomes null.


Resolvers

13. What is a resolver and what arguments does it receive?

A resolver is a function that returns the value for a field. Every field has a resolver — if not explicitly defined, the default resolver reads the property from the parent object.

const resolvers = {
  Query: {
    user: (parent, args, context, info) => {
      return context.db.users.findById(args.id);
    },
  },
  User: {
    posts: (parent, args, context, info) => {
      return context.db.posts.findByUserId(parent.id);
    },
  },
};
Argument Contents
parent Return value of the parent resolver (root resolvers get {})
args Arguments passed to the field in the query
context Shared object across all resolvers (DB, auth, DataLoaders)
info Query AST, field path, return type metadata

14. How does the GraphQL resolver chain work?

Resolvers execute in a top-down, breadth-first order. Each resolver returns a value (or a Promise), and child resolvers run after their parent resolves.

Query.user()          → { id: "1", name: "Alice" }
  User.posts()        → [{ id: "10" }, { id: "11" }]
    Post.title()      → default resolver: parent.title
    Post.author()     → fetch user for each post

The engine waits for all sibling promises at each level before moving to the next level.

15. What is the N+1 problem in GraphQL?

When fetching a list and then a related field for each item, the naive approach fires one query for the list plus N queries for the related data.

query {
  posts {       # 1 query: SELECT * FROM posts
    title
    author {    # N queries: SELECT * FROM users WHERE id = ?
      name
    }
  }
}

With 100 posts, this is 101 database queries. This is the N+1 problem.

16. How does DataLoader solve the N+1 problem?

DataLoader batches and caches individual loads within a single tick of the event loop.

import DataLoader from 'dataloader';

// Batch function: receives array of keys, returns array of values (same order)
const userLoader = new DataLoader(async (userIds: readonly string[]) => {
  const users = await db.users.findMany({
    where: { id: { in: userIds as string[] } },
  });
  // Must return values in the same order as keys
  return userIds.map(id => users.find(u => u.id === id) ?? null);
});

// In resolver — called N times, but only 1 DB query fires
const resolvers = {
  Post: {
    author: (post, _, context) => context.userLoader.load(post.authorId),
  },
};

// Attach to context per request (new loader per request for cache isolation)
const context = ({ req }) => ({
  db,
  userLoader: new DataLoader(batchUsers),
});

Instead of N queries, DataLoader coalesces all .load() calls into a single SELECT … WHERE id IN (…).

17. What is the context object and what should go in it?

Context is created once per request and passed to every resolver. It should contain:

  • Authenticated user (decoded JWT)
  • Database client / ORM instance
  • DataLoader instances (one per request for cache isolation)
  • Feature flags, request metadata
const server = new ApolloServer({
  typeDefs,
  resolvers,
  context: ({ req }) => {
    const token = req.headers.authorization?.replace('Bearer ', '');
    const user = token ? verifyJWT(token) : null;
    return {
      user,
      db,
      loaders: createLoaders(db),
    };
  },
});

Authentication & authorization

18. How do you implement authentication in GraphQL?

The standard pattern is to validate a JWT (or session) in the context function, then access context.user in resolvers.

// Context function — runs before every request
context: ({ req }) => {
  const auth = req.headers.authorization ?? '';
  if (auth.startsWith('Bearer ')) {
    try {
      const user = jwt.verify(auth.slice(7), process.env.JWT_SECRET!);
      return { user, db };
    } catch {
      // Token invalid — context.user will be null
    }
  }
  return { user: null, db };
},

// Resolver — checks context.user
Query: {
  me: (_, __, ctx) => {
    if (!ctx.user) throw new GraphQLError('Not authenticated', {
      extensions: { code: 'UNAUTHENTICATED' },
    });
    return ctx.db.users.findById(ctx.user.id);
  },
},

19. How do you implement field-level authorization?

Two common approaches:

1. In-resolver checks:

Post: {
  secretNotes: (post, _, ctx) => {
    if (ctx.user?.role !== 'ADMIN') return null; // or throw
    return post.secretNotes;
  },
},

2. Schema directives (declarative):

directive @auth(requires: Role = USER) on FIELD_DEFINITION

type Post {
  title: String!
  secretNotes: String @auth(requires: ADMIN)
}

Libraries like graphql-shield and pothos provide structured permission systems.

20. What is the difference between authentication and authorization in GraphQL?

Concept What Where
Authentication Who is making the request? Context function — runs once per request
Authorization What are they allowed to do? Resolvers or schema directives — runs per field

Pagination

21. What are the main pagination approaches in GraphQL?

Approach Pros Cons
Offset (limit/offset) Simple, easy to implement Inconsistent with inserts/deletes, slow on large offsets
Cursor-based Stable with mutations, consistent Stateful cursors, harder to jump to page N
Relay connection spec Standardised, works with most clients More verbose schema

22. What is the Relay Cursor Connection Specification?

Relay's connection spec provides a standard cursor-based pagination shape:

type PostConnection {
  edges: [PostEdge!]!
  pageInfo: PageInfo!
  totalCount: Int!
}

type PostEdge {
  node: Post!
  cursor: String!
}

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

type Query {
  posts(first: Int, after: String, last: Int, before: String): PostConnection!
}

A cursor is typically a base64-encoded primary key or composite value. first/after paginate forward; last/before paginate backward.


Error handling

23. How does GraphQL handle errors?

GraphQL can return partial success: valid data is returned alongside errors for failed fields. The response always has data and optionally errors.

{
  "data": {
    "user": {
      "name": "Alice",
      "posts": null
    }
  },
  "errors": [
    {
      "message": "Could not load posts",
      "path": ["user", "posts"],
      "locations": [{ "line": 5, "column": 5 }],
      "extensions": {
        "code": "INTERNAL_SERVER_ERROR"
      }
    }
  ]
}

24. What are error extensions and error codes?

Extensions provide machine-readable metadata on errors:

throw new GraphQLError('Resource not found', {
  extensions: {
    code: 'NOT_FOUND',
    http: { status: 404 },
    entityType: 'Post',
    entityId: args.id,
  },
});

Standardised codes (Apollo convention): BAD_USER_INPUT, UNAUTHENTICATED, FORBIDDEN, NOT_FOUND, INTERNAL_SERVER_ERROR.

25. Should you use non-null (!) for mutation return types?

Generally no for the root mutation field. Returning a nullable type allows you to communicate failure via errors while still providing partial data. A non-null mutation that throws will null-propagate and erase any other data in the response.

# Preferred — nullable allows partial response
type Mutation {
  createPost(input: CreatePostInput!): Post  # nullable
}

# Union error pattern (explicit error types)
union CreatePostResult = Post | ValidationError | AuthError

type Mutation {
  createPost(input: CreatePostInput!): CreatePostResult!
}

Performance

26. What are persisted queries and why do they matter?

Persisted queries store query strings server-side indexed by a hash. Clients send only the hash, not the full query text.

Benefits:

  • Smaller HTTP payloads (hash vs full SDL string)
  • Prevents arbitrary query execution in production (security)
  • Enables GET requests + HTTP caching for queries

Apollo Client supports automatic persisted queries (APQ) out of the box.

27. How do you limit query depth and complexity?

Unlimited depth or complexity can cause expensive recursive queries (DOS risk):

import depthLimit from 'graphql-depth-limit';
import { createComplexityLimitRule } from 'graphql-validation-complexity';

const server = new ApolloServer({
  typeDefs,
  resolvers,
  validationRules: [
    depthLimit(7),                    // max 7 levels deep
    createComplexityLimitRule(1000),  // max complexity score
  ],
});

Complexity scores assign weights to fields (e.g., list fields cost more than scalar fields).

28. How does caching work in GraphQL?

HTTP-level GET caching works with persisted queries (same hash = same cache key). For field-level caching:

# Apollo Server @cacheControl directive
type Post {
  id: ID!
  title: String! @cacheControl(maxAge: 60)      # cache 60s
  author: User   @cacheControl(scope: PRIVATE)  # don't cache publicly
}

For more granular caching, use DataLoader (request-level cache) or external cache (Redis) in resolvers.

29. What is query batching?

Some GraphQL clients (Apollo) can batch multiple queries into a single HTTP request:

[
  { "query": "query { user(id: \"1\") { name } }" },
  { "query": "query { posts { title } }" }
]

The server processes them and returns an array of results. Apollo Server supports batching out of the box. This reduces round-trips but increases latency for independent queries that could resolve in parallel.


Subscriptions

30. How do GraphQL subscriptions work?

Subscriptions maintain a persistent connection between client and server, pushing updates when subscribed events occur.

type Subscription {
  postCreated: Post!
  messageReceived(roomId: ID!): Message!
}
// Server — using graphql-ws or subscriptions-transport-ws
const resolvers = {
  Subscription: {
    postCreated: {
      subscribe: (_, __, { pubsub }) => pubsub.asyncIterableIterator('POST_CREATED'),
    },
  },
  Mutation: {
    createPost: async (_, { input }, { db, pubsub }) => {
      const post = await db.posts.create(input);
      pubsub.publish('POST_CREATED', { postCreated: post });
      return post;
    },
  },
};

Transport options:

  • WebSocket (graphql-ws library) — full duplex, preferred
  • Server-Sent Events (SSE) — one-way, works through HTTP/2 proxies

31. What are the challenges with GraphQL subscriptions at scale?

  • WebSocket connections are stateful — harder to load balance than HTTP
  • Each subscription event triggers N resolver calls (one per connected client)
  • PubSub must be shared across server instances (Redis pub/sub or Kafka)
  • Connection limits: one WS per browser tab, thousands possible per server

Production pattern: use a dedicated subscription server or a managed service (Ably, Pusher, Hasura).


Apollo & tooling

32. What is Apollo Federation?

Apollo Federation splits a large GraphQL schema into independently deployable subgraphs, unified by a supergraph through the Apollo Router.

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

# Posts subgraph — extends User from Users subgraph
type Post {
  id: ID!
  title: String!
  author: User! @provides(fields: "name")
}

extend type User @key(fields: "id") {
  id: ID! @external
  posts: [Post!]!
}

The Apollo Router handles query planning: splitting a client query across subgraphs and merging results.

33. What is schema stitching and how does it differ from federation?

Schema stitching (older approach) merges schemas programmatically at the gateway level using SDL merging and type merging. Schemas must be aware of each other.

Apollo Federation (newer): Each subgraph declares its own types with federation directives (@key, @external, @provides, @requires). The supergraph is composed at build time using the Rover CLI. Subgraphs are fully independent.

Federation is the recommended approach for most multi-team deployments.

34. What is the difference between Apollo Client's cache policies?

Apollo Client normalises query results in a flat in-memory cache keyed by typename:id.

Fetch policy Behaviour
cache-first Read from cache; hit network only on miss (default)
cache-and-network Return cached data immediately, then update from network
network-only Always hit network; write to cache
no-cache Always hit network; do not write to cache
cache-only Read from cache only; error on miss
standby Like cache-only but no automatic refresh

Advanced topics

35. What is the SDL-first vs code-first approach?

Approach Description Tools
SDL-first Write .graphql schema files; resolvers implement it Apollo Server, graphql-tools
Code-first Generate SDL from TypeScript classes/decorators Pothos, TypeGraphQL, NestJS GraphQL

Code-first avoids schema/resolver divergence through static types; SDL-first keeps the schema human-readable and allows non-JS teams to contribute.

36. How do you test GraphQL APIs?

// Unit test — resolver in isolation
describe('userResolver', () => {
  it('returns user by id', async () => {
    const mockDb = { users: { findById: jest.fn().mockResolvedValue({ id: '1', name: 'Alice' }) } };
    const result = await resolvers.Query.user(null, { id: '1' }, { db: mockDb }, null);
    expect(result).toEqual({ id: '1', name: 'Alice' });
  });
});

// Integration test — full server
import { ApolloServer } from '@apollo/server';

const { body } = await server.executeOperation({
  query: `query { user(id: "1") { name } }`,
});
expect(body.singleResult.data?.user?.name).toBe('Alice');

For E2E, use graphql-request or Apollo Client's MockedProvider.

37. What is the info argument in a resolver?

info contains the query AST and field metadata. It is rarely needed but useful for:

  • Lookahead: Determine which sub-fields were requested to optimise DB queries (e.g., skip a JOIN if the field wasn't requested)
  • Field path: Know where in the query tree execution is
  • Return type: Inspect the expected type for dynamic logic
import { parseResolveInfo } from 'graphql-parse-resolve-info';

Post: {
  author: (post, args, ctx, info) => {
    const fields = parseResolveInfo(info);
    // Only load profile if it was requested
    const includeProfile = !!fields?.fieldsByTypeName?.User?.profile;
    return ctx.db.users.findById(post.authorId, { includeProfile });
  },
},

38. What are custom directives and when would you use them?

Custom directives annotate schema elements and can transform field behaviour:

directive @upper on FIELD_DEFINITION
directive @auth(requires: Role!) on FIELD_DEFINITION
directive @rateLimit(max: Int!, window: String!) on FIELD_DEFINITION

Implementation (using mapSchema from @graphql-tools/utils):

function upperDirectiveTransformer(schema: GraphQLSchema) {
  return mapSchema(schema, {
    [MapperKind.OBJECT_FIELD]: (fieldConfig) => {
      const directive = getDirective(schema, fieldConfig, 'upper')?.[0];
      if (directive) {
        const { resolve = defaultFieldResolver } = fieldConfig;
        return {
          ...fieldConfig,
          resolve: async (...args) => {
            const result = await resolve(...args);
            return typeof result === 'string' ? result.toUpperCase() : result;
          },
        };
      }
    },
  });
}

39. What is the @defer and @stream directive?

@defer defers slow parts of a response and streams them incrementally:

query {
  user(id: "1") {
    name              # returned immediately
    ... @defer {
      friendCount     # streamed when ready
      recentActivity  # streamed when ready
    }
  }
}

@stream streams list items one by one:

query {
  posts @stream(initialCount: 5) {
    title
  }
}

These are incremental delivery features using multipart HTTP responses. Support varies by server library.

40. What is schema introspection and should you disable it in production?

Introspection allows clients to query the full schema. In production:

  • Disable for public-facing APIs to prevent schema enumeration
  • Keep enabled for internal/developer tools
  • Use Apollo Studio's managed schema instead of public introspection
new ApolloServer({
  introspection: process.env.NODE_ENV !== 'production',
});

GraphQL vs REST

41. When should you choose REST over GraphQL?

Choose REST when Choose GraphQL when
Simple CRUD with predictable access patterns Complex, interconnected data
Public API consumed by third parties unfamiliar with GraphQL Mobile clients needing bandwidth efficiency
Heavy HTTP caching requirements Multiple clients with different data needs
Team lacks GraphQL experience Rapid UI iteration without backend changes
File uploads are a primary use case Data graph / relationship-heavy domain
Existing REST ecosystem is sufficient Type-safe, self-documenting API

42. How do you handle file uploads in GraphQL?

The graphql-multipart-request-spec extends GraphQL over multipart form data:

scalar Upload

type Mutation {
  uploadAvatar(file: Upload!): String!
}
import { GraphQLUpload, graphqlUploadExpress } from 'graphql-upload';

app.use(graphqlUploadExpress());

const resolvers = {
  Upload: GraphQLUpload,
  Mutation: {
    uploadAvatar: async (_, { file }) => {
      const { createReadStream, filename, mimetype } = await file;
      const stream = createReadStream();
      // pipe to S3, disk, etc.
      return savedUrl;
    },
  },
};

Note: File uploads are complex with Federation; many teams use a separate REST endpoint for uploads and GraphQL only for metadata.


Practical scenarios

43. How would you design a GraphQL schema for an e-commerce site?

type Product {
  id: ID!
  name: String!
  price: Float!
  stock: Int!
  category: Category!
  images: [Image!]!
  reviews(first: Int, after: String): ReviewConnection!
}

type Cart {
  id: ID!
  items: [CartItem!]!
  subtotal: Float!
  itemCount: Int!
}

type CartItem {
  product: Product!
  quantity: Int!
  lineTotal: Float!
}

type Query {
  product(id: ID!): Product
  products(category: ID, minPrice: Float, maxPrice: Float, first: Int, after: String): ProductConnection!
  cart: Cart
}

type Mutation {
  addToCart(productId: ID!, quantity: Int!): Cart!
  updateCartItem(productId: ID!, quantity: Int!): Cart!
  removeFromCart(productId: ID!): Cart!
  checkout(input: CheckoutInput!): Order!
}

type Subscription {
  orderStatusChanged(orderId: ID!): Order!
}

44. How do you implement real-time notifications with GraphQL?

// Server — subscription with filtering
Subscription: {
  notification: {
    subscribe: withFilter(
      (_, __, { pubsub }) => pubsub.asyncIterableIterator('NOTIFICATION'),
      (payload, variables, context) => {
        // Only push to the correct user
        return payload.notification.userId === context.user.id;
      }
    ),
  },
},

// Publish from mutation or background job
await pubsub.publish('NOTIFICATION', {
  notification: { userId: targetUserId, message: 'Order shipped!' },
});

45. How do you implement search with filters in GraphQL?

input ProductFilter {
  category: ID
  minPrice: Float
  maxPrice: Float
  inStock: Boolean
  tags: [String!]
}

enum SortField { PRICE CREATED_AT NAME POPULARITY }
enum SortOrder { ASC DESC }

input ProductSort {
  field: SortField!
  order: SortOrder! = ASC
}

type Query {
  products(
    filter: ProductFilter
    sort: ProductSort
    first: Int = 20
    after: String
  ): ProductConnection!
}

Common mistakes

Mistake Problem Fix
Not using DataLoader N+1 queries, poor performance Create DataLoaders per request in context
Shared DataLoader across requests Cache pollution between users New DataLoader instance per request
Over-using non-null (!) Error nullifies parent chain Use nullable on complex/joined fields
Exposing DB IDs as opaque IDs Client has to know they're integers Use UUIDs or base64-encoded global IDs
Introspection enabled in production Schema enumeration attack Disable introspection in prod config
Mutations returning Boolean Client can't know what changed Return the modified object
Fat resolvers Hard to test, violates SRP Extract business logic into service layer
No depth/complexity limits DoS via deeply nested queries Add graphql-depth-limit and complexity rules

GraphQL vs REST vs gRPC

Dimension REST GraphQL gRPC
Protocol HTTP HTTP (any) HTTP/2
Message format JSON JSON Protocol Buffers
Schema Optional (OpenAPI) Mandatory (SDL) Mandatory (.proto)
Client control None — server decides Full — client specifies fields None — server decides
Real-time Polling / SSE Subscriptions (WS/SSE) Streaming RPCs
Browser native Yes Yes No (needs grpc-web)
Code generation Optional Optional Mandatory
Best for Public APIs, CRUD Complex graphs, mobile Internal microservices, streaming

6 FAQ

Q: Is GraphQL always better than REST? No. REST is simpler, easier to cache, and better understood. GraphQL excels when multiple clients have diverging data needs, or when the data model is a graph. Use GraphQL where it solves a real problem — not by default.

Q: How do you version a GraphQL API? GraphQL avoids versioning through deprecation: add new fields, deprecate old ones, remove only after clients migrate. If breaking changes are unavoidable, run two schemas in parallel (e.g., /graphql/v1 vs /graphql) during transition.

Q: Can GraphQL coexist with REST in the same project? Yes. Many teams add GraphQL incrementally alongside REST, or use GraphQL as a gateway over REST microservices (using @graphql-tools/wrap).

Q: How does GraphQL affect SEO? GraphQL is a transport layer — it doesn't affect SEO directly. Server-side rendering (Next.js, Nuxt) still works; the HTML sent to crawlers is the same.

Q: What is the biggest performance risk with GraphQL? The N+1 problem and unbounded query complexity. Both are solvable (DataLoader + depth/complexity limits), but require intentional configuration.

Q: What is the difference between Apollo Server 3 and Apollo Server 4? Apollo Server 4 introduced a framework-agnostic startStandaloneServer, removed the built-in Express integration (now expressMiddleware), deprecated apollo-server-* packages in favour of @apollo/server, improved TypeScript support, and improved context typing. The configuration API changed significantly.

Keep reading

All Toolmingotools are free & run in your browser

No sign-up, no upload, no watermark. Your files never leave your device.

Browse all tools