
GraphQL interview questions test whether you can turn a typed API contract into a reliable UI. For frontend roles, the interviewer is not only checking syntax. They want to hear how a query maps to a screen, how mutations update visible state, how partial data and errors behave, and how client choices affect backend cost.
The best answers do not say "GraphQL is better than REST." They explain where GraphQL helps, where REST is simpler, and what can go wrong when a flexible query language reaches production.
Use GraphQL Learn, GraphQL over HTTP, GraphQL response format, GraphQL security guidance, GraphQL pagination, GraphQL caching, and Apollo Client cache docs for current terminology.
If REST comparison is your weak spot, pair this with REST API Interview Questions for Frontend Devs and practice explaining the same product screen in both API styles.
| Area | What to know | How to answer |
|---|---|---|
| Schema | Object types, fields, nullability, enums, interfaces | Explain how schema shape becomes frontend data shape |
| Operations | Queries, mutations, subscriptions, variables, fragments | Show how operations map to user actions |
| Caching | Normalized cache, IDs, field policies, invalidation | Explain what changes after a mutation |
| Errors | Partial data, errors, network failures | Separate GraphQL execution errors from transport errors |
| Security and cost | Auth, authorization, depth, persisted documents | Say GraphQL does not remove backend authorization |
Backend interviews may spend more time on resolver implementation, schema governance, and database access. Frontend interviews usually probe the client contract:
If your answer stays at the definition level, it will sound memorized. Tie every concept to a page, component, request, mutation, or production failure.
GraphQL gives the frontend a typed schema and lets the client select the fields needed for a view. That can reduce overfetching and underfetching, especially when one screen needs data that would otherwise require several REST endpoints.
The tradeoff is that GraphQL shifts work into schema design, resolvers, caching, authorization, and query cost control. A good answer says GraphQL improves the data contract when the graph is designed well, not that it automatically makes every request faster.
Common mistake: Saying "GraphQL prevents overfetching" without mentioning that resolvers can still fetch too much or do expensive backend work.
A query reads data. A mutation represents a write or state-changing operation. A subscription streams updates over a long-lived connection such as WebSocket or server-sent events.
For frontend work, the distinction matters because each operation drives different UI states. A query needs loading, empty, success, partial-error, and retry states. A mutation needs pending, optimistic, success, rollback, validation-error, and duplicate-submit handling. A subscription needs connection, reconnect, stale-event, and auth-expiry behavior.
Interview-ready add-on: A mutation should usually return the changed object or payload the UI needs to update the cache. Returning only success: true often forces a refetch or leaves the client guessing.
A schema is the typed contract exposed by a GraphQL server. It defines which operations are available, which object types exist, which fields can be selected, what arguments are accepted, and which values can be null.
For a frontend engineer, the schema is a working client contract. It shapes generated types, component fragments, form input objects, error handling, and the long-term compatibility of old clients.
type Product {id: ID!name: String!price: Money!imageUrl: String}type Query {product(id: ID!): Product}
Common mistake: Treating the schema as a JSON response shape. The schema is a contract with validation, nullability, field ownership, and evolution rules.
Scalars are leaf values such as String, Int, Float, Boolean, and ID. Object types contain fields that can be selected. Enums limit a value to a known set of names. Input types describe structured arguments for queries and mutations.
enum SortDirection {ASCDESC}input ProductFilterInput {query: StringinStockOnly: Boolean}
Input types are separate from output object types because output types can contain computed fields, relationships, interfaces, unions, or resolver-only behavior that does not make sense as input.
String! means the field must not be null. [Product!]! means the list itself is not null and none of its items are null. Nullability is a product contract because it decides whether the UI can show partial data or must treat the parent object as unavailable.
If a non-null field resolves to null during execution, GraphQL adds an error and bubbles the null up to the nearest nullable parent. Overusing ! can make a small resolver failure blank out a larger part of the response.
Good answer shape: Explain the UI consequence. A nullable imageUrl lets the product card render a placeholder. A non-null price says the card is not useful without price, so failure should be more visible.
Variables keep the GraphQL document stable while passing dynamic values separately. They are validated by type, easier to reuse, safer than string-building, and friendlier to operation caching or persisted documents.
query ProductDetails($id: ID!) {product(id: $id) {idnameprice {amountcurrency}}}
Variables:
{ "id": "product_123" }
Common mistake: Building query strings with template literals and user input. That makes validation, caching, escaping, and operation tracking harder.
Aliases rename response keys. They are useful when querying the same field more than once with different arguments, or when the UI needs two differently named versions of the same schema field.
query CompareProducts {left: product(id: "product_1") {idname}right: product(id: "product_2") {idname}}
Without aliases, both selections would produce the same product key and conflict.
Fragments define reusable field selections. They are most useful when components own their data needs, because the fragment can live near the component that renders those fields.
fragment ProductCardFields on Product {idnameimageUrlprice {amountcurrency}}
Fragments improve consistency, but they can also hide overfetching if teams create one large shared fragment for every screen. A useful fragment represents what a component actually renders.
Directives annotate parts of a GraphQL document or schema to change behavior. Common built-in directives include @include, @skip, and @deprecated.
query ProductDetails($id: ID!, $showReviews: Boolean!) {product(id: $id) {idnamereviews @include(if: $showReviews) {ratingbody}}}
For frontend interviews, explain directives as part of the data contract. @include can conditionally fetch a field, while @deprecated tells clients a schema field is being phased out.
A resolver is the server-side function that returns the value for a schema field. It may read from a database, call another API, compute a value, or delegate to another service.
Frontend engineers are not always expected to implement resolvers, but they should understand the cost boundary. One GraphQL request can trigger many resolver calls behind the scenes, so a harmless-looking nested query can be expensive.
const resolvers = {Query: {product: (_parent, args, context) => {return context.productService.findById(args.id);},},};
Good answer shape: Mention the usual resolver inputs: parent value, field arguments, request context, and execution info. Then connect that to auth, batching, and observability.
A GraphQL response can contain data and errors together. Request errors, such as syntax, validation, or variable-coercion errors, stop execution and do not include data. Execution errors happen at a response position and may produce partial data; older GraphQL material often calls these field errors.
{"data": {"product": {"id": "product_123","name": "Everyday Backpack","inventory": null}},"errors": [{"message": "Inventory service unavailable","path": ["product", "inventory"]}]}
The UI should not automatically turn every GraphQL error into a full-page failure. If the missing field is non-critical, render the safe data and show a local fallback. If the failed field is essential, show a stronger error state.
Authentication identifies the caller. Authorization decides whether that caller can read or mutate a resource or field. Both must be enforced on the server side, usually in resolvers or the service layer behind them.
Do not rely on hiding fields or buttons in the UI. A user can still send a GraphQL operation manually. The frontend can improve UX by hiding unavailable actions, but the server must remain the source of truth.
Good answer shape: Mention authentication headers or cookies, per-resource authorization, consistent error behavior, and audit logging for sensitive mutations.
GraphQL does not define client caching by itself. Many clients normalize objects by type name and ID, then store fields separately so different queries can reuse the same entity.
For example, Apollo Client can identify an object using __typename plus id by default. That means a product returned by a list query and a product returned by a detail query can point to the same cache entity.
Cache correctness depends on stable IDs, selected fields, field arguments, pagination merge policy, and mutation responses. If a mutation response omits the changed object's ID, the client may not know which cached entity to update.
A mutation should return enough data for the client to update the UI safely. That might be the changed entity, affected list metadata, aggregate counts, or a domain-specific payload with validation errors.
mutation UpdateCartLine($input: UpdateCartLineInput!) {updateCartLine(input: $input) {cart {idtotalItemssubtotal {amountcurrency}}line {idquantity}userErrors {fieldmessage}}}
Returning only success: true is weak because the frontend still needs to refetch, guess, or manually patch state without enough information.
Optimistic UI updates the interface before the server confirms the mutation. It works well when the operation is likely to succeed and the rollback behavior is clear.
For a cart quantity change, the frontend can show the new quantity immediately, disable repeated clicks while the mutation is pending, and roll back if the server rejects the change because of stock, price, or permission rules.
What to say in interviews: Name the visible states: optimistic value, pending indicator, duplicate-submit protection, rollback, validation error, and final cache consistency.
Use cursor-based pagination for feeds and changing lists when possible. The connection pattern returns edges, node, cursor, and pageInfo, which lets the UI request the next page without relying on unstable offsets.
query ProductList($first: Int!, $after: String) {products(first: $first, after: $after) {edges {cursornode {idname}}pageInfo {hasNextPageendCursor}}}
The frontend still needs to dedupe by stable ID, preserve scroll position, handle empty states, and avoid appending stale pages after filters change.
N+1 happens when resolving a list triggers one extra backend call per item. The frontend sees one GraphQL request, but the server may perform many database or service calls.
Example: a products query returns 40 products, and each product's seller field calls the seller service separately. That becomes one call for the list plus 40 calls for sellers.
Common fixes include batching, request-scoped caching, joining data earlier, or using loader patterns. A frontend engineer should know enough to ask whether expensive fields are batched and whether resolver timing is observable.
Use layered demand control. For first-party clients, trusted documents can allow only known operations in production. For broader APIs, use pagination, depth limits, breadth or alias limits, batch limits, query complexity analysis, rate limits, timeouts, and monitoring.
Turning off introspection is not enough. It can reduce schema discoverability in non-development environments, but it does not replace authorization, input validation, trusted documents, or operation limits.
Interview-ready answer: "I would control both who can call the API and how expensive each operation can be."
Persisted documents store approved GraphQL operations on the server and let clients send an operation ID, often a hash, instead of the full document. Trusted documents go further by treating those stored operations as an allowlist for first-party production clients.
They can improve security and operations because the server can reject unknown arbitrary documents, track known operation names, and reason about query cost ahead of time.
They do not remove authorization. A known operation can still request data the current user should not see unless the server checks permissions during execution.
Introspection lets clients query the schema itself. It powers tools, autocomplete, documentation, and code generation.
Disabling introspection can reduce schema discoverability for a private first-party API, but it also removes standard tooling and is not a security boundary. Public GraphQL APIs often leave it enabled. In either case, production security must rely on authorization, trusted documents where appropriate, demand control, and safe error messages.
GraphQL can support file uploads through conventions, but uploads often fit better as direct-to-storage or REST-style flows. The frontend concern is not only "can GraphQL upload a file?" It is progress, cancellation, retry, size limits, auth, virus scanning, and what happens if metadata succeeds but upload fails.
A practical answer says: use GraphQL for metadata and signed upload coordination when that keeps the API clean, but do not force large binary transfer through GraphQL just to be consistent.
Use a custom scalar when a value needs domain-specific serialization or validation, such as DateTime, URL, EmailAddress, or JSON.
The tradeoff is that clients need to know the runtime format. A schema can say DateTime, but frontend code still needs documentation or generated scalar mappings that explain whether the value is an ISO string, number, or custom object.
An interface defines shared fields that multiple object types must implement. A union says a field may return one of several object types without requiring shared fields.
Use an interface when the types share a real contract:
interface Node {id: ID!}
Use a union for heterogeneous results such as search:
union SearchResult = Product | Article | Brand
In frontend code, both usually require checking __typename before rendering type-specific fields.
Prefer additive schema evolution. Add new fields, deprecate old fields, track operation usage, migrate clients, and remove fields only after the compatibility window.
Avoid changing a field's meaning in place. That is worse than removing it because old clients may keep working syntactically while showing wrong data.
Whole endpoint versioning can exist, but it is usually a last resort for major compatibility breaks. The normal GraphQL path is schema evolution through additive changes and deprecation.
Generated types catch mismatches between operations and frontend code. They help when a selected field can be null, when a union needs narrowing, or when a mutation input changes.
They can hurt if teams treat generated types as architecture. Codegen cannot fix a vague schema, unstable IDs, unsafe nullability, or a mutation that returns too little data. It is a guardrail, not a replacement for schema ownership.
Subscriptions keep a long-lived connection open and push updates when server-side events happen. They are useful for chat, notifications, live dashboards, collaborative editing signals, and other flows where the user benefits from immediate updates.
Avoid subscriptions for data that can be refreshed on navigation, loaded on demand, or polled cheaply. Subscriptions add connection state, auth renewal, reconnect logic, ordering concerns, and server resource cost.
GraphQL is not tied to one transport, but HTTP is commonly used for queries and mutations. Many GraphQL APIs send operations to a single endpoint such as /graphql. Servers commonly support POST, and some support GET for queries when it is safe and useful for caching.
Do not assume HTTP status codes carry every application outcome. A well-formed operation that executes can return HTTP 200 and still contain GraphQL execution errors alongside partial data. Parse or validation failures and transport failures use different status handling, so the client must inspect both the HTTP result and the GraphQL response body.
REST can be simpler for file downloads, uploads, public cacheable resources, small CRUD APIs, webhook-style integrations, and teams without schema governance. REST also maps naturally to HTTP caching and resource URLs.
That does not make GraphQL bad. It shows judgment. Choose GraphQL when the product has multiple clients, nested data needs, strong schema benefits, or frontend screens that suffer from overfetching, underfetching, and endpoint coordination.
Start at the user-visible symptom, then split the problem:
A good answer does not blame GraphQL first. It follows the request from component to client cache to network to resolver execution to render.
Choose one screen with clear ownership and measurable behavior. Keep the existing REST flow as a reference, design the GraphQL query around the fields the screen actually renders, generate types, handle loading and error states, compare response size and latency, and verify analytics or logs before removing the old path.
Do not migrate every endpoint just because GraphQL is available. A safe migration proves that the schema, cache, authorization, and observability work for one product flow first.
These examples are short on purpose. In an interview, explain the tradeoff around each one instead of only reading the syntax.
fragment ProductCardFields on Product {idnameimageUrlprice {amountcurrency}}query ProductGrid($query: String!, $first: Int!, $after: String) {products(filter: { query: $query }, first: $first, after: $after) {edges {cursornode {...ProductCardFields}}pageInfo {hasNextPageendCursor}}}
What this shows: stable variables, component field ownership, cursor pagination, and a response shape the UI can merge page by page.
mutation SaveProduct($input: SaveProductInput!) {saveProduct(input: $input) {product {idisSavedsavedCount}userErrors {fieldmessage}}}
What this shows: the mutation returns the changed entity and fields the UI needs. The client can update a product card, detail page, and saved counter without guessing.
{"data": {"product": {"id": "product_123","name": "Everyday Backpack","recommendations": null}},"errors": [{"message": "Recommendation service timed out","path": ["product", "recommendations"]}]}
What this shows: the product page can render core product data while showing a fallback for recommendations. The answer should depend on whether the failed field is critical.
Use scenarios because they reveal whether the concept survives messy product work. Try answering each prompt in two minutes, then name what you would inspect before choosing a solution.
| Scenario | What a good answer should reason through |
|---|---|
| A feed query gets slower as users follow more accounts | Resolver cost, pagination, batching, query limits, cache policy, and backend indexes |
| A field cannot be removed because old clients use it | Deprecation, schema evolution, operation tracking, and migration windows |
| A frontend needs partial data with errors | GraphQL response shape, nullable fields, error handling, and UI fallback states |
| A public GraphQL endpoint is abused | Trusted documents, auth, rate limiting, cost analysis, and monitoring |
| A mutation updates several visible screens | Return shape, optimistic update, cache write, invalidation, rollback, and refetching |
Each answer below hides the schema, client, or failure decision the interviewer needs to hear. Replace the slogan with the concrete GraphQL tradeoff.
| Weak answer | Why it falls short | Better direction |
|---|---|---|
| "GraphQL is always better than REST" | The tradeoff depends on clients, caching, team ownership, and server cost | Explain when GraphQL helps and when REST is simpler |
| "No overfetching means no performance problem" | Resolvers can still do expensive work | Mention demand control and backend query planning |
| "Disable introspection and it is secure" | Security cannot rely on hiding the schema | Use auth, authorization, operation limits, and monitoring |
| "Fragments are only for reuse" | Fragments also affect cache consistency and component data contracts | Discuss colocated data needs and maintenance |
| "Types mean the API is safe" | Type checks do not prove business permission or runtime availability | Mention auth, nullability, resolver failures, and testing |
| Level | What the answer should show |
|---|---|
| Fresher | Can write queries and mutations, understands schema basics, variables, loading states, and error states |
| Mid-level | Can reason about cache updates, pagination, fragments, partial data, and when REST is simpler |
| Senior | Can discuss schema evolution, resolver cost, N+1, security, observability, generated types, and migration strategy |
| Staff/lead | Can design GraphQL boundaries across teams, define governance, protect the API from expensive operations, and keep client contracts sane |
A product listing page is a useful GraphQL interview example because it touches schema design, client state, pagination, caching, authorization, and performance. A shallow answer says GraphQL prevents overfetching. A stronger answer explains the data contract and the cost of each field.
Start with the schema shape:
type ProductConnection {edges: [ProductEdge!]!pageInfo: PageInfo!totalCount: Int}type ProductEdge {cursor: String!node: Product!}type Product {id: ID!name: String!imageUrl: Stringprice: Money!inventoryStatus: InventoryStatus}type Query {products(filter: ProductFilterInputsort: ProductSortInputfirst: Int!after: String): ProductConnection!}
Then explain the frontend flow:
A good spoken answer might be:
I would query products with filter, sort, first, and after variables. Product cards would own a fragment for id, name, image, price, and inventory status. I would keep filters in the URL, merge cursor pages by product ID, and show a local fallback if a non-critical field fails. Before shipping, I would check resolver timing for inventory and recommendation fields because one GraphQL request can still trigger expensive backend work.
When practicing, make one answer concrete enough to draw: the operation, response shape, cache entries affected, error state, and server check. That exposes missing reasoning faster than another definition drill.
Prepare for REST API interview questions as a frontend fresher with answers on HTTP methods, status codes, fetch, CORS, auth, caching, pagination, errors, and API contracts.
Prepare for Databricks frontend interviews with practical questions, official round expectations, CoderPad tips, and frontend system design practice.
