GraphQL is a query language and runtime for APIs that consists of three fundamental building blocks: the Schema (type definitions), Resolvers (functions that fetch data), and Queries/Mutations (client operations). Its architecture follows a client-server model where clients request exactly the data they need, and the server responds with a shaped JSON response matching the request.
GraphQL is a query language for APIs and a runtime for executing those queries against existing data. Unlike REST, which exposes multiple endpoints for different resources, GraphQL exposes only a single endpoint and allows clients to request precisely the data they need, reducing over-fetching and under-fetching. The architecture is built around a strongly typed schema that defines the capabilities of the API, with resolvers implementing how each field is populated.
Schema: The core contract between client and server, defining all possible data types, queries, mutations, and subscriptions. Written in GraphQL Schema Definition Language (SDL), it serves as the documentation and validation layer for the API.
Types: The building blocks of the schema. Object types define shapes of data (e.g., type User { id: ID! name: String }). Scalar types are primitive values (String, Int, Float, Boolean, ID). Custom scalars can extend these.
Queries: Read operations that fetch data. Clients specify exactly which fields they need, and the server returns only those fields. Queries can be nested to retrieve related data in a single request.
Mutations: Write operations that modify data (create, update, delete). Like queries, they return data, allowing clients to get the updated state in the same request.
Subscriptions: Real-time operations that maintain a persistent connection and push data when events occur. Implemented over WebSockets.
Resolvers: Functions that execute the logic for each field in the schema. Each field in a type has a corresponding resolver that fetches or computes its value. Resolvers can be hierarchical, allowing for efficient data fetching.
Input Types: Special object types used for mutation arguments, allowing complex structured input (e.g., input UserInput { name: String! email: String }).
GraphQL can be implemented in several architectural patterns depending on the use case:
GraphQL Gateway (Schema Stitching): A single GraphQL layer that sits above existing REST APIs, databases, or microservices. The gateway aggregates data from multiple sources and presents a unified GraphQL schema. This pattern is ideal for migrating legacy systems or building an API facade.
Federated GraphQL: Multiple GraphQL services (subgraphs) are composed into a single federated graph using Apollo Federation or similar tools. Each subgraph owns its part of the schema, and the gateway handles query planning and resolution across services. This is the primary pattern for microservices architectures.
Direct Database Access: The GraphQL server connects directly to databases (SQL, NoSQL) and resolves data using database queries. This simplifies the stack but couples the GraphQL schema tightly to the data model.
Code-First vs Schema-First: Schema-first (SDL-first) approach defines types in SDL and then implements resolvers. Code-first (resolver-first) uses programming language constructs to define the schema programmatically (e.g., TypeGraphQL, Nexus, graphql-js).
Client Sends Query: Client sends a GraphQL document to the single endpoint (typically /graphql). The query specifies exactly which fields are needed.
Parsing and Validation: Server parses the query into an abstract syntax tree (AST) and validates it against the schema. Any syntax or type errors are returned immediately.
Execution: The server starts at the root Query or Mutation type and calls resolvers for each requested field. Resolvers can be asynchronous and may call other resolvers, creating a dependency graph.
Data Fetching: Resolvers fetch data from databases, REST APIs, or other services. The resolver pattern allows for optimized batching and caching (using DataLoader).
Response: The server assembles the result into a JSON object matching the shape of the query. Errors are returned alongside partial data when applicable.
One of GraphQL's architectural challenges is the N+1 query problem. In the resolver example above, fetching a list of users and their posts could trigger one query for users and then N queries for posts (one per user). DataLoader solves this by batching and caching requests. It collects all pending requests, deduplicates them, and executes them in a single batched query. DataLoader is considered essential for production GraphQL servers to prevent performance degradation.