LearnThatStack Ace your next interview

GraphQL

Apollo Server 5, Apollo Client 4 and Federation 2 Checked Oct 2026 57 code snippets

The model

  • The schema is the contract. It gives every field a type, and the server checks each query against it before running it.
  • The client names the fields it wants. The response comes back in the same shape as the query.
  • Every field has a resolver, the function that returns its value. That value becomes the parent of the fields under it.
  • A failed field becomes null and adds an entry to errors. The rest of the data still arrives, usually with HTTP 200.
  • Every operation goes to one endpoint, usually as a POST. That is why HTTP caching takes more work than in REST.
Clientapp or browserGraphQL serverparse, validateResolversone per fieldData sourcesDB, REST, servicesASKPOST /graphql one URLREFUSEerrors only if invalidRUNexecute field by fieldfetch batched by keyREPLYdata + errors HTTP 200
The server checks a query against the schema first. A bad one gets errors and no data. A good one runs a resolver for each field.

GraphQL vs REST

what it fixes, what it costs, and how to move over

Requests over HTTP

Send any operationPOST /graphqlA JSON body with query, variables and operationName.
Send a query a CDN can cacheUse GET, with the query in the URL. Mutations must use POST.
Know the status of a partial failureUsually 200, with data and errors side by side.

TrapMonitoring that reads only status codes misses most GraphQL errors. Read the body too.

GraphQL's downsides

Name GraphQL's main downsidesExpensive queries and harder rate limiting, since one request can do little work or a lot.
Know when REST is the better fitA small API with one client. A schema and resolvers are more work than it needs.

GraphQL or gRPC

Call between backend servicesgRPC. Binary Protocol Buffers over HTTP/2, with generated client stubs.
Serve web and mobile appsGraphQL. Browsers call it directly. gRPC needs gRPC-Web and a proxy.

TipMany systems use both, with gRPC between services and GraphQL in front of the apps.

Move from REST to GraphQL

Start without changing the backendclass UserAPI extends RESTDataSourceResolvers call the existing REST endpoints.
Design the new schemaModel what the screens need, not one field per endpoint.

TrapResolvers that call REST once per item bring the N+1 problem back. Batch them with DataLoader where REST has a bulk endpoint.

Schema & types

the typed contract the server checks every query against

Types, fields and null

datausername!email
null moves upname! failed. Its nearest nullable parent, user, became null, so email was lost too.
Define an object typetype User { id: ID! }
Make a field non-nullname: String!Fields are nullable unless their type ends in !.
Make a list and its items non-nullposts: [Post!]!
Add the entry points for readingtype Query { me: User }

Scalars, enums and inputs

Name the five built-in scalarsInt, Float, String, Boolean and ID. ID is sent as a string.
Add a custom scalarscalar DateServer code parses and validates its values.
Allow only a fixed set of valuesenum Role { ADMIN USER }
Pass an object as an argumentinput NewUser { name: String! }

TrapYou can't use an object type like User as an argument. Inputs hold only scalars, enums and other inputs, or lists of them.

Interfaces and unions

Make types share fieldstype User implements Node
Return one of several unrelated typesunion Result = User | Post
Choose between themAn interface when the types share fields, a union when they share none.

TrapA union has no shared fields. Every field except __typename needs an inline fragment.

Schema-first or code-first

Agree on the API before writing codeSchema-first. Write the Schema Definition Language (SDL) by hand, then resolvers to match.
Keep one source of truth in TypeScriptCode-first. A library like TypeGraphQL builds the schema from classes.

TipIn code-first, the schema exists only once the code runs. Generate the SDL to review changes.

Change the schema safely

Replace a field without breaking clients@deprecated(reason: "Use name")Add the new field and mark the old one. Remove it once usage tracking shows no client needs it.
Catch a breaking change before releaseDiff the schema in your build pipeline.

TrapOld mobile app versions stay installed, so they keep asking for old fields.

Queries, fragments & paging

ask for exactly the fields a screen needs

Directives

Add or drop a field with a flagemail @include(if: $full)posts @skip(if: $lite)
Name the directives the spec defines@include, @skip, @deprecated and @specifiedBy, plus @oneOf for inputs that take exactly one field.

Page through a list

Shape a paginated fieldA Relay connection. Each edge holds a node and its cursor, beside a pageInfo object.
Get the next pageusers(first: 10, after: $cursor)Pass the last endCursor as after.
Work out hasNextPageFetch one row more than first. If it comes back, there is a next page.
Choose cursors over offsetsOffsets slow down on big tables, and skip or repeat rows when data changes.

Resolvers & Apollo Server

how the server fills in each field

Write a resolver

Know the four resolver arguments(parent, args, context, info)
Give every resolver the user and the DBPut them on context. The server builds it once per request.
Skip writing a resolverThe default resolver reads the property with the field's name from parent.
Wait on a database callReturn a promise or use async. Start independent calls together with Promise.all.

How a request runs

Name the three phasesParse into a syntax tree (AST), validate it against the schema, then execute it.
Say what the resolver chain isEach resolver's result becomes the parent of the fields nested under it.
Know what a thrown error doesThe field becomes null, and its error and path go into errors.

Test the API

Unit-test a resolverCall it as a plain function, with mock args and context.
Run a query through the whole serverserver.executeOperation({ query })

TrapApollo Server 3 dropped apollo-server-testing and its createTestClient, which old guides still use.

N+1 & caching

stop one query from turning into hundreds of database calls

Spot the N+1 problem

postspostspostsqueryqueryquery
N+1Each posts resolver sends its own query, one per user.
postspostspostsquery
DataLoaderThe loader collects the three user IDs and sends one query.
Count the database calls1 for the list, plus 1 per item. 100 users make 101 calls.
Say why GraphQL causes itA list field runs its child resolver once per item.

TrapSmall development data hides it, so N+1 often shows up only in production.

Batch with DataLoader

Write the batch functionnew DataLoader(batchFn)It gets every key from one tick of the event loop. Return one value per key, in the same order.
Load one key from a resolverloaders.posts.load(user.id)
Scope loaders to one requestCreate new ones in the context function.

TrapA loader shared across requests keeps serving values cached for earlier requests.

Cache GraphQL responses

Cache whole responsesKey them by query and variables, in a store like Redis.
Give a field a cache hint@cacheControl(maxAge: 60)A response lives as long as its lowest maxAge.
Let a CDN cache a querySend it as GET, often as a persisted query hash.

TrapKeep user-specific data out of shared caches.

Persisted queries

Send a hash instead of the query textAutomatic persisted queries, or APQ. The client sends the full text only when the server asks.
Run only the queries your apps shipTrusted documents, an allowlist you register at build time.

TrapAPQ blocks nothing, since any client can register any query. Only an allowlist stops attackers.

Mutations & errors

design writes, and report what failed

Design a mutation

Take one input argumentcreatePost(input: CreatePostInput!)
Return a payload typeCreatePostPayloadIt holds the changed objects, so the client can update its view.
Name it for what the user doesLike publishPost, not one generic update for the whole record.

Return errors

Report an error the user can fixAs data in the payload, like errors: [UserError!], each with a field and a code.
Put each error type in the schemaunion Result = User | EmailTaken
Throw an error with a codeextensions: { code: 'USER_NOT_FOUND' }Pass it to GraphQLError.
Hide internal details from clientsformatErrorSwap database messages for a generic one, and log the real error.

TrapKeep the top-level errors array for failures the client can't fix, like an outage.

Wrap writes in a transaction

Make a mutation's writes all or nothingRun them in one database transaction, in the resolver or its service.
Stop two transfers passing the same balance checkLock the rows with SELECT ... FOR UPDATE.
Send emails and events safelySend them after the commit, so a rollback never announces a change that didn't happen.

TrapEach top-level mutation field commits on its own. If a later one fails, the earlier one stays.

Upload files

Upload without sending the file through GraphQLA mutation returns a signed URL. The client uploads straight to storage.
Send a file in a GraphQL requestscalar UploadFollow the multipart request spec, with graphql-upload.

TrapMultipart requests skip the browser's preflight, so they need CSRF protection. Apollo Server blocks them without an Apollo-Require-Preflight header.

Auth & security

know who is calling, and cap what one query costs

Authenticate requests

Check the token once per requestIn the context function, from the Authorization header.
Reject a user who isn't logged incode: 'UNAUTHENTICATED'Throw a GraphQLError with this code. Apollo Server 4 removed AuthenticationError.

TrapAuthentication only says who the user is. Each field still needs an authorization check.

Authorize each field

Keep access rules in one placeIn a business logic layer, as graphql.org recommends.
Mark protected fields in the schema@auth(requires: ADMIN)It needs server code to enforce it.
Hide a field from other usersIts resolver returns null unless the viewer owns the record.

TrapA check only at the top misses other paths to the same field. Check access on each field.

Limit expensive queries

Cap how deep a query nestsdepthLimit(10)
Reject a query that costs too muchmaximumCost: 1000A list multiplies its children's cost by first. So 50 users with 50 posts each count as 2,500 posts.
Cap what one request can doCap aliases, batched operations and page sizes.
Rate-limit by cost, not by countGive each client a cost budget. One request can be cheap or huge.
Time out a slow queryAt the router or HTTP server, plus a shorter timeout on each DB call.

Trapgraphql-rate-limit counts in memory by default. Use its RedisStore to share counts across servers.

Hide the schema

Say what introspection isThe built-in __schema query, which returns every type and field. Tools build docs from it.
Turn off introspectionintrospection: falseApollo Server 4 and later turn it off by default when NODE_ENV is production.
Hide the field suggestions in errorshideSchemaDetailsFromClientErrors: true

TrapHiding the schema is only obscurity. Authorization and cost limits are the real defense.

Apollo Client

fetch, cache and update data in React

Set up Apollo Client

Create a clientnew ApolloClient({ link, cache })
Fetch data in a componentuseQuery(GET_USER)
Get a function that runs a mutationuseMutation(ADD_TODO)

TrapApollo Client 4 removed the constructor's uri option and moved the hooks to @apollo/client/react.

Asked in interviews

AskedWhat is Apollo Client?

The normalized cache

ROOT_QUERYUser:1Post:7Post:8
cacheThe cache stores each object once, by type and id. Other entries point to it by reference.
Know an object's cache keyUser:1Its __typename plus its id.
Know the default fetch policyfetchPolicy: 'cache-first'A query fully in the cache skips the network.
Show a new item in a cached listWrite an update function, or refetch the list.

TrapHave each mutation return the id and changed fields. Otherwise the cache can't update the object.

Optimistic UI

Show a mutation's result before the server repliesoptimisticResponseThe object you expect back, with __typename and id.
Handle a failed mutationApollo Client rolls back the optimistic copy. Show an error too.

TrapSkip it where a false success is costly, like a payment.

Generate types

Generate TypeScript types from the schemanpx graphql-codegen
Type each operation's resultpreset: clientIt generates a typed graphql() function.

TrapThe old apollo CLI no longer generates code. Apollo recommends GraphQL Code Generator.

Subscriptions

push live updates to clients

Subscribe to events

Subscribe to new messagessubscription { messageAdded { id } }A subscription takes exactly one root field.
Know when polling is betterWhen data changes rarely. It needs no open connection.

Serve subscriptions

Pick the transportA WebSocket running graphql-ws. Server-Sent Events are the main alternative.
Add subscriptions to Apollo ServerRun expressMiddleware beside a graphql-ws server. startStandaloneServer has no support for them.
Publish an event from a mutationpubsub.publish('MSG', payload)
Send each client only its eventswithFilter(subscribe, filterFn)
Run on more than one serverSwap the in-memory PubSub for one backed by Redis or Kafka.

Trapsubscriptions-transport-ws is deprecated and doesn't work with graphql-ws.

Federation & stitching

build one graph from many services

Apollo Federation

Split one graph across teamsEach team runs a subgraph. A router plans each query across them.
Add fields to a type another subgraph ownstype User @key(fields: "id")That shared type is an entity.
Load an entity from its key__resolveReference

TrapIn Federation 2, skip extend type and @external on the key. Just declare the type with @key again.

Found this useful? Pass it on.
Pro · $10/mo

This sheet stays free. With Pro, you can:

  • Every answer on this sheet. Read the full answer to all 60 questions it links, not just the 9 free ones.
  • My sheet. The cards you star and the ones you miss in Quiz me, from every sheet, on one page to quiz and print.
  • PDFs. Download this sheet and My sheet, laid out for A4, with every question a link to its answer.
  • The rest of Pro. Every answer in the question library, and 1,000 AI credits a month.