A GraphQL request is sent as a JSON payload (typically via HTTP POST) to a single endpoint, containing a query string and optional variables. The server parses, validates, and executes the query by invoking resolvers in a depth-first order, collecting and assembling the requested data into a response that mirrors the query structure.
GraphQL operates over a single endpoint (usually /graphql) and uses HTTP POST by convention. Unlike REST where each resource has its own endpoint, the client sends a structured query that specifies exactly what data it needs. The server processes this query through a pipeline of parsing, validation, and execution, then returns a JSON response that matches the shape of the query. All of this happens in a single round trip, eliminating over-fetching and under-fetching.
Query String: The client constructs a GraphQL document defining the operation. This includes field selections and can include fragments for reuse.
Variables: Dynamic values are passed separately in a variables object, preventing injection attacks and allowing query reuse.
Operation Name: Optional but recommended when multiple operations exist in one document.
HTTP Method: POST is standard, though GET can be used for queries (with query string in URL) with proper caching headers.
Headers: Authentication tokens, content-type, and other headers are sent as with any HTTP request.
Parse: The server receives the raw string and parses it into an Abstract Syntax Tree (AST). If syntax errors exist, the server returns a 400 error with details.
Validate: The AST is validated against the schema. This checks that fields exist, types match, and required arguments are provided. Validation happens before execution, ensuring malformed queries are rejected early.
Execute: The server traverses the AST depth-first, invoking resolvers for each selected field. The execution is driven by the query's selection set, not the schema.
Collect: Resolver results are collected and assembled into a data object that mirrors the query structure.
Return: The final JSON response is serialized and sent back to the client, with errors (if any) in a separate errors array.
The resolver execution creates a directed graph based on the query. Starting from the root Query or Mutation, resolvers are invoked depth-first. Each resolver can fetch data from different sources (databases, REST APIs, caches) and can be asynchronous. Parent resolvers pass their result to child resolvers via the parent argument. This allows for hierarchical data fetching: a User resolver might return a user object, and the posts field resolver for that user uses the user's id to fetch associated posts.
data: Contains the result of the query if execution succeeded. Mirrors the shape of the query exactly. Null fields may appear if data was not found.
errors: Array of error objects, each with a message, path (to the field that caused error), and optional extensions. Errors do not block partial data—successful fields are still returned.
extensions: Optional metadata for tooling, performance timing, or custom debugging info.
The resolver chain can create performance challenges, particularly the N+1 problem where fetching a list of users and their posts would trigger N+1 database queries. DataLoader solves this by batching and caching requests. It collects all requests for a particular data source during a single execution cycle and issues them in a single batched query. This is a critical optimization for production GraphQL servers.