DS DevShelfHub Projects · AI tools
Cheatsheets / GraphQL
Cheatsheet · Dev tooling

GraphQL: Schema, Queries and Resolvers Reference Guide

By DevShelfHub

Schema, types, queries, mutations, subscriptions, resolvers, pagination, federation — the API design surface.

91 items 7 min Schema Resolvers Federation

Start hereQuick start · 6 you’ll reach for daily

Schematype Post { id: ID! }
Query{ posts { id title } }
Mutationmutation { create(input: …) }
Resolver(parent, args, ctx) => …
Batchnew DataLoader(…)
Codegengraphql-codegen --config …

spec & toolingVersions

Targets: graphql spec: October 2021 @apollo/server ≥ 4 graphql-js ≥ 16

Snippets use Apollo Server because it’s the most widely deployed Node implementation; concepts map cleanly to graphql-yoga, Strawberry, and Hot Chocolate. Subscriptions assume graphql-ws transport (the WebSocket protocol that replaced subscriptions-transport-ws in 2021).

install · pick a serverSetup

bash
# Server picks
npm install @apollo/server graphql                # Apollo Server (most popular)
npm install graphql-yoga                          # smaller, modular alternative
npm install pothos-graphql graphql @pothos/core   # code-first schema (TS)

# Python / FastAPI
pip install strawberry-graphql[fastapi]

# Client picks
npm install @apollo/client graphql                # full-featured cache
npm install graphql-request                       # minimal fetch wrapper
npm install urql                                  # smaller, exchange-based

# Dev tooling
npm install -D graphql-codegen-cli @graphql-codegen/typescript
npx graphql-codegen init                           # generate TS types from schema

where things liveCommon imports

import { ApolloServer } from "@apollo/server"Server constructor.
import { startStandaloneServer } from "@apollo/server/standalone"Quickest way to bind to a port.
import { expressMiddleware } from "@apollo/server/express4"Mount under Express.
import { ApolloClient, InMemoryCache, gql, HttpLink } from "@apollo/client"Apollo Client basics.
import { useQuery, useMutation, useSubscription, useLazyQuery } from "@apollo/client"React hooks.
import DataLoader from "dataloader"Per-request batch + cache. Solves N+1.
import { buildSchema, graphql } from "graphql"Lower-level graphql-js primitives.
import { createYoga } from "graphql-yoga"Yoga alternative server.
import { request } from "graphql-request"Minimal client.

SDL typesSchema

type Post { … }Object type. Fields with return types.
id: ID!Trailing ! means non-null.
tags: [Tag!]!Non-null list of non-null Tags (preferred for return types).
enum Status { DRAFT PUBLISHED }Fixed set of string values.
interface Node { id: ID! }Implemented by other types. Foundation of Relay.
type User implements Node { … }Implementation must include interface fields.
union SearchResult = Post | UserDisjoint result types.
scalar DateTimeCustom scalar — supply parse/serialise in resolvers.
input CreatePostInput { … }Argument-only types (no fields with resolvers).
@deprecated(reason: "Use newField")Mark fields as deprecated. Visible to introspection.
type Query / Mutation / SubscriptionThree root operation types.
markdown
# Schema Definition Language (SDL)
scalar DateTime

"""A blog post."""
type Post {
  id:        ID!
  title:     String!
  body:      String!
  status:    PostStatus!
  author:    User!
  tags:      [Tag!]!
  createdAt: DateTime!
}

enum PostStatus { DRAFT PUBLISHED ARCHIVED }

interface Node { id: ID! }
type User implements Node {
  id:    ID!
  name:  String!
  posts(first: Int = 10): PostConnection!
}

union SearchResult = Post | User

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

type Query {
  post(id: ID!): Post
  posts(status: PostStatus, first: Int = 20, after: String): PostConnection!
}

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

type Subscription {
  postPublished: Post!
}

read operationsQueries

{ posts { id title } }Anonymous query.
query Posts($status: PostStatus) { … }Named query with variables.
post(id: $id) { … }Pass variables as args.
{ a: posts(status: DRAFT) { id } b: posts(status: PUBLISHED) { id } }Aliases for two calls of the same field.
fragment PostBits on Post { id title }Reusable selection set.
...PostBitsSpread a fragment.
... on Post { title }Inline fragment for unions / interfaces.
@include(if: $cond) / @skip(if: $cond)Conditional inclusion of a field.
__typenameAlways queryable. Required for union / interface narrowing.

write operationsMutations

mutation CreatePost($input: CreatePostInput!) { createPost(input: $input) { id } }Standard shape: named, single input arg, return updated entity.
type CreatePostPayload { post: Post errors: [UserError!]! }Payload wrapper for user-facing errors.
type UserError { field: String! message: String! }Validation errors as data, not GraphQL errors.
Multiple top-level mutationsExecute serially in the order they appear (queries are parallel).
Idempotency via inputAdd a clientMutationId or idempotencyKey.
Return enough to update the cacheIds + fields the UI needs. Saves a round-trip.

push from serverSubscriptions

type Subscription { postPublished: Post! }One field = one event channel.
graphql-ws (WebSocket)Preferred Modern transport.
subscriptions-transport-wsLegacy Older WS protocol.
SSE / multipart over HTTPUse when WS isn’t available (some CDNs).
resolver returns AsyncIteratorYield events; server pushes to subscribers.
PubSub: in-memory / Redis / KafkaCross-process fan-out.
Authenticate at connectWS connections persist; per-message auth is too late.

how data is fetchedResolvers

(parent, args, context, info) => valueFour-argument signature.
context shared per requestAuth, dataloaders, DB handles. Built in context().
Default resolverIf absent, returns parent[fieldName].
async resolverReturn a Promise. Engine awaits before serialising.
DataLoader.load(key)Batches calls in the same tick. Solves N+1.
@auth / @rateLimit / custom directivesCross-cutting concerns at field level.
throw new GraphQLError("…", { extensions: { code: "UNAUTHENTICATED" } })Structured errors. Field becomes null.
return null for nullable, raise for non-nullNon-null field error propagates to nearest nullable parent.
code-first (Pothos / Nexus / TypeGraphQL)TS-driven schema. No SDL strings.
javascript
// Apollo Server resolvers map 1:1 to schema fields
import { ApolloServer } from "@apollo/server";
import { startStandaloneServer } from "@apollo/server/standalone";
import DataLoader from "dataloader";

const typeDefs = /* GraphQL */ `
  type Post  { id: ID!, title: String!, author: User! }
  type User  { id: ID!, name: String! }
  type Query { posts: [Post!]! }
`;

const resolvers = {
  Query: {
    posts: (_: unknown, _args: unknown, ctx: Ctx) => ctx.db.posts.all(),
  },
  Post: {
    // Per-row author lookup batched by DataLoader -> no N+1
    author: (post, _args, ctx: Ctx) => ctx.loaders.user.load(post.authorId),
  },
};

type Ctx = { db: DB; loaders: { user: DataLoader } };

const server = new ApolloServer({ typeDefs, resolvers });
await startStandaloneServer(server, {
  context: async () => ({
    db,
    loaders: {
      user: new DataLoader(async (ids) => db.users.byIds(ids as string[])),
    },
  }),
});

connections & cursorsPagination

first / after, last / beforeRelay-spec args. Cursor-based.
type PostConnection { edges: [PostEdge!]! pageInfo: PageInfo! totalCount: Int }Standard connection wrapper.
type PostEdge { node: Post! cursor: String! }Edge holds node + opaque cursor.
type PageInfo { hasNextPage: Boolean! endCursor: String }Standard pageInfo fields.
offset / limitSimpler but bad for stable order over time.
cursor encoding: base64(jsonish)Opaque to the client; encode index / timestamp.
cap first server-sideOtherwise first: 999999 is an availability bug.

two kinds of failureErrors

GraphQL errors (top-level errors array)System / auth / parse failures. Field is null; consumer is forced to handle.
User errors (in payload)Preferred for validation. Typed data, queryable.
extensions.code = "UNAUTHENTICATED" / "FORBIDDEN" / "BAD_USER_INPUT"Apollo conventions for typed error codes.
Don’t leak stack tracesStrip in prod via formatError.
Non-null propagationAn error in a non-null field bubbles up to the nearest nullable ancestor.
Always return arrays even on no results[Post!]! → empty array, not null.

batching, caching, persistedPerformance

DataLoader per requestCritical. Without it, N+1 lurks behind every field-level resolver.
Query complexity / depth limitsgraphql-depth-limit / cost analysis. Reject pathological queries.
Persisted queriesHash → document map. Smaller payloads, server-side allowlist.
APQ (Automatic Persisted Queries)Apollo: client sends hash; server fetches doc on miss.
@defer / @streamServer streams parts of the response as ready (incremental delivery).
N+1 dashboard / Apollo traceAudit field-level resolver counts.
Cache HTTP responsesGET + persisted queries make CDN caching tractable.

composed schemasFederation

Subgraph schemas, one gatewayApollo Federation, Wundergraph, Mesh, Hive Router.
@key(fields: "id")Mark an entity that other subgraphs can extend.
type User @key(fields: "id") { id: ID! }Entity is fetchable across the graph.
extend type User @key(fields: "id") { posts: [Post!]! }Other subgraph adds fields to User.
@external / @requires / @providesResolver dependencies across subgraphs.
@shareable / @inaccessible / @overrideGovern overlap between subgraphs.
Schema registryApollo GraphOS, Hive. Composition + breaking-change checks in CI.

browser sideClient

useQuery(DOC, { variables, skip, fetchPolicy })Reactive query hook.
useMutation(DOC, { refetchQueries, optimisticResponse })Mutation hook. Optimistic UI built in.
useSubscription(DOC)Subscribe inside a component.
cache.identify(obj) / writeFragment / readFragmentHand-edit the normalized cache.
typePolicies.{TypeName}.keyFieldsOverride default id-based normalisation.
fetchPolicy: "cache-and-network"Render cache, then refetch in background.
Codegen with GraphQL Code GeneratorGenerate TS types + typed hooks from your schema + operations.
javascript
// Apollo Client — typical query + mutation hook
import { ApolloClient, InMemoryCache, gql, useQuery, useMutation } from "@apollo/client";

export const client = new ApolloClient({
  uri: "/graphql",
  cache: new InMemoryCache({
    typePolicies: {
      User: { keyFields: ["id"] },
      Query: { fields: { posts: { keyArgs: ["status"] } } },   // for pagination merge
    },
  }),
});

const POSTS = gql`
  query Posts($status: PostStatus) {
    posts(status: $status) {
      edges { node { id title } cursor }
      pageInfo { hasNextPage endCursor }
    }
  }
`;
const CREATE_POST = gql`
  mutation Create($input: CreatePostInput!) { createPost(input: $input) { id title } }
`;

function PostList() {
  const { data, loading, fetchMore } =
    useQuery(POSTS, { variables: { status: "PUBLISHED" } });
  const [createPost] = useMutation(CREATE_POST, { refetchQueries: [POSTS] });
  // ...
}

schema + server + hand-rolled clientEnd-to-end · Notes API

Schema, resolvers, standalone server, plus a plain fetch-based client — proves you don’t need a client library to talk to GraphQL.

javascript
// End-to-end: schema -> resolvers -> server -> hand-rolled client fetch.
import { ApolloServer } from "@apollo/server";
import { startStandaloneServer } from "@apollo/server/standalone";

const typeDefs = /* GraphQL */ `
  type Note { id: ID! body: String! }
  type Query    { notes: [Note!]! }
  type Mutation { addNote(body: String!): Note! }
`;

const notes: { id: string; body: string }[] = [];
const resolvers = {
  Query:    { notes: () => notes },
  Mutation: {
    addNote: (_: unknown, { body }: { body: string }) => {
      const n = { id: crypto.randomUUID(), body };
      notes.push(n); return n;
    },
  },
};

const server = new ApolloServer({ typeDefs, resolvers });
const { url } = await startStandaloneServer(server, { listen: { port: 4000 } });
console.log(`GraphQL at ${url}`);

// Hand-rolled client (no library)
const res = await fetch(url, {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    query: `mutation($b: String!) { addNote(body: $b) { id body } }`,
    variables: { b: "hello" },
  }),
});
const { data, errors } = await res.json();

Best practiceGood to know

Wrap every mutation result in a payload type. createPost: Post! looks tidy but leaves you no place to put user-facing validation errors. createPost: CreatePostPayload! with post + errors ages well.
Use DataLoader per-request, not globally. Build them in your context() function. Sharing across requests = a cache-poisoning bug waiting to happen.
Generate types from your schema. graphql-codegen on the client (typed hooks) and on the server (typed resolvers) eliminates an entire class of “works in dev, fails in prod” bugs.

Common trapsWatch out for

No query budget = DoS risk. posts(first: 999999) { author { posts { author … } } } can blow up the server. Add depth + cost limits before exposing publicly.
Non-null fields propagate failure. One error in a deeply nested non-null field bubbles up and nulls a much larger slice of the response. Make “might fail” fields nullable.
Schema breaking changes are expensive. Once clients ship, you can’t remove a non-null field or change a return type. Use @deprecated, add new fields, retire later. Plug a schema registry into CI.

Go deeperSee also

GraphQL FAQ

What is GraphQL used for?

GraphQL is a query language and runtime for APIs that lets clients request exactly the data they need. Unlike REST, a single GraphQL endpoint handles queries for reading data, mutations for writing data, and subscriptions for real-time updates — all in one request.

What is the difference between GraphQL queries and mutations?

Queries fetch data and are read-only; mutations modify server-side data (create, update, delete). Both follow the same typed schema, but mutations carry the semantic meaning that something will change, which affects caching behavior and allows optimistic updates in clients.

What is a GraphQL resolver?

A resolver is a function that returns the value for one field in the schema. It receives the parent object, the field arguments, and a context object. Resolvers can call a database, another API, or a DataLoader to batch and deduplicate requests.

How does GraphQL federation work?

Federation lets you split a GraphQL schema across multiple independent services called subgraphs. Each subgraph owns a slice of the schema and a router (Apollo Router or graphql-mesh) merges them into a single supergraph. This enables team autonomy without a monolithic schema file.

Is GraphQL better than REST?

GraphQL and REST solve different problems. GraphQL excels when clients need flexible data shapes, when you want to reduce over-fetching, or when building a single API consumed by multiple front-end clients. REST is simpler, cacheable at the HTTP layer, and better documented by default.