Basics
- Schema
- Federated Schemas
- Best Practices
- Overview
- Type Modifiers
- Structure & Types
- Pagination
GraphQL is a query language for APIs and a runtime for executing those queries. It allows clients to request exactly the data they need, making it more efficient and flexible compared to traditional REST APIs. It's not tied to any specific database or storage engine and supports reading, writing, and subscribing to data changes, enabling real-time updates.
GraphQL Schema: blueprint that defines the structure of the data in a GraphQL API, including types, queries, mutations, and subscriptions. It specifies how clients can interact with the data.
GraphQL services can be written in any language, and there are various approaches to defining types in a schema:
- might require you to construct schema types, fields, and resolver functions using the same programming language as the GraphQL implementation
- will allow you to define types and fields using the schema definition language (SDL) and write resolver functions separately
- will let you write and annotate resolver functions, inferring the schema from them
- may infer both types and resolver functions based on underlying data sources
Benefits
- Schema Design
- schema-first Development. Design the GraphQL schema first using SDL (Schema Definition Language) to ensure a clear API contract before implementation
- Type Definitions
- use Query, Mutation, and Subscription root types for operations
- define reusable input and object types for consistent and modular schema components
- Scalability
- language Agnostic. Not tied to any specific database or storage engine
- modularize the schema using type extensions and federated architecture for microservices
- enforce Apollo Federation for distributed schemas
- Versioning Strategy
- adopt schema deprecation strategies using
@deprecated
directives - maintain backward compatibility while evolving the schema iteratively
- adopt schema deprecation strategies using
- Performance Optimization
- Fetching - process of retrieving specific data from a server or database. GraphQL can fetch from multiple sources in a single query and it eliminates the over-fetching and under-fetching dilemma
- Over-Fetching: occurs when clients receive more data than needed that leads to increased network traffic with resource wastage and longer response times
- Under-Fetching: occurs when insufficient data is provided for tasks that leads to inadequate information for decision-making, data inconsistencies, increased latency, and user frustration
- define precise field-level resolvers to minimize N+1 query problems
- use
DataLoader
to batch and cache backend data-fetching for optimized resolver performance - implement query complexity analysis to restrict overly complex queries
- Fetching - process of retrieving specific data from a server or database. GraphQL can fetch from multiple sources in a single query and it eliminates the over-fetching and under-fetching dilemma
- Developer Experience
- provide a comprehensive, auto-generated GraphQL schema documentation using tools like GraphiQL
- implement linting rules for schema and query validation
- use a type-safe approach by integrating GraphQL with TypeScript
Modifier | Definition | Example |
---|---|---|
Non-Null | Indicates the field is required |
|
List | Indicates that a field can return a list of values. A list can contain zero or more items of the specified type |
|
List Variant | [1, 2] | [] | null | [1, null] | [[1], [2,3]] | [[]] | [[1], null] | [[null]] |
---|---|---|---|---|---|---|---|---|
[Int] | β | β | β | β | β | β | β | β |
[Int!] | β | β | β | β | β | β | β | β |
[Int]! | β | β | β | β | β | β | β | β |
[Int!]! | β | β | β | β | β | β | β | β |
[[Int]] | β | β | β | β | β | β | β | β |
[[Int!]] | β | β | β | β | β | β | β | β |
[[Int!]!] | β | β | β | β | β | β | β | β |
[Int!]!]! | β | β | β | β | β | β | β | β |
Type | Definition | Example |
---|---|---|
Object | GraphQL Object type, indicating it has fields. Most schema types will be Object types |
|
Build-In Scalar Type |
|
|
Custom Scalar Type | User-defined data type that extends built-in scalars to represent specific formats, allowing for serialization and deserialization of complex types like dates or URLs |
|
Enum | Scalar type that can take a limited set of predefined values, helping to enforce data constraints in a GraphQL API |
|
Input Object | Custom type that defines fields for input parameters in queries or mutations, enabling structured transmission of complex data |
|
Interface | Abstract type that defines fields for multiple object types to implement, allowing for shared fields and enabling queries across different types through a common interface |
|
Union | Type that can represent one of several different object types, allowing for more flexible responses without requiring shared fields among the types |
|
Build-In Directives | Directives may be provided in a specific syntactic order which may have semantic interpretation |
|
Custom Directives | User-defined annotations that can be applied to fields or fragments in a GraphQL schema to modify their behavior |
|
Alias | Allows clients to rename the result of a field in a query, enabling clients to fetch the same field multiple times with different parameters |
|
Fragments | Reusable units of a GraphQL query that allow clients to define a set of fields that can be included in multiple queries. Fragments help reduce duplication and improve query organization |
|
Query | Read operation in GraphQL that allows clients to request specific data from the server. Queries specify the shape of the response by defining the fields to retrieve |
|
Mutation | Write operation in GraphQL that allows clients to modify server-side data. Mutations can create, update, or delete data and typically return the modified data |
|
Subscription | Real-time operation in GraphQL that allows clients to receive updates from the server when specific events occur. Subscriptions enable clients to listen for changes to data |
|
Top-Level Queries | Queries should be well-defined and self-contained, with a focus on clear entry points |
|
Entity Relationships | Use relationships to define nested data |
|
Deeply Nested Structures | Limit the depth and use fragments to handle complexity |
|
Descriptions | Comments that can be added to schema definitions to provide additional context or documentation for fields, types, or directives |
|
Comments | Used to annotate the schema or queries for clarity and documentation purposes |
|
Pagination in GraphQL is done via standardized pagination model (cursor-based) using Connections
Pagination Algorithm
- Server applies cursors (
before
,after
) to filteredges
- Fetch all relevant records
- Slice the records based on indices (
first
,last
) - Create
edges
. For each record in the sliced list, create an edge object that includes the cursor (encoded identifier) and the node (the record itself) - Determine
pageInfo
. Check for additional records after the current slice to sethasNextPage
and, if they exist, calculate theendCursor
for the last record in the current slice - Create a
Connection
object that includesedges
andpageInfo
{
user {
id
name
"""
Slicing is done with the `first` argument to followers (get first 10 followers)
Pagination is achieved using the `after` argument with friends (passed a cursor to request friends following that cursor)
"""
followers(first: 10, after: "opaqueCursor") {
"""
For each edge, requested a cursor, an opaque string used for pagination with the `after` argument
"""
edges {
cursor
node {
id
name
}
}
pageInfo {
"""
Requested hasNextPage to determine if more edges are available or if we've reached the end of the connection
"""
hasNextPage
}
}
}
}
Edges to Return | Apply Cursors to Edges |
---|---|
|
|
Structure
Including both first
and last
is strongly discouraged due to potential confusion in queries and results
type Query {
users(input: UserInput): UserConnection!
}
input UserInput {
pagination: PaginationInput
}
input PaginationInput {
first: Int
after: String
last: Int
before: String
}
type UserConnection {
edges: [UserEdge]!
pageInfo: PageInfo!
}
type PageInfo @shareable {
hasNextPage: Boolean!
hasPreviousPage: Boolean!
startCursor: String
endCursor: String
}
type UserEdge {
cursor: String!
node: User!
}
type User {
id: ID!
name: String!
}
- Reserved Types: GraphQL server must reserve specific types and names to support the pagination model of connections
- Any object ending in
Connection
- An object named
PageInfo
- Any object ending in
- Connection Types: Any type ending in
Connection
is classified as a Connection Type. Connection types must be an Object. Must include fields namededges
andpageInfo
. They may also have additional fields as determined by the schema designer- Edges: Connection Type must include a field called
edges
, which returns a list type wrapping an edge type - PageInfo: Connection Type must include a field called
pageInfo
, which returns a non-null PageInfo object
- Edges: Connection Type must include a field called
- Edge Types: List returned by a connection type's edges field and must be an Object. Must include fields named
node
andcursor
. They may also have additional fields as determined by the schema designer- Node: Must have a
node
field that returns a Scalar, Enum, Object, Interface, Union, or a Non-Null wrapper of those types, but not a list - Cursor: Must have a
cursor
field that returns a type serializing as a String. This can be a String, a Non-Null String, a custom scalar that serializes as a String, or a Non-Null wrapper around such a custom scalar
- Node: Must have a
- Arguments: Field that returns a Connection Type must include forward pagination arguments, backward pagination arguments, or both. These arguments enable the client to slice the set of edges before it is returned
- Forward pagination arguments: Provide a non-negative integer
first
and a cursorafter
to return edges after the specified cursor, with a maximum offirst
edges - Backward pagination arguments: Provide a non-negative integer
last
and a cursorbefore
to return edges before the specified cursor, with a maximum oflast
edges - Edge Order: Order the edges based on business logic and additional arguments, but the ordering must remain consistent across pages, ensuring that when using
first/after
, the edge closest to the cursor comes first, while withlast/before
, it comes last
- Forward pagination arguments: Provide a non-negative integer
- PageInfo: Provides metadata about the current page of results. Must include
hasPreviousPage
andhasNextPage
as non-null booleans, andstartCursor
andendCursor
as opaque strings.startCursor
andendCursor
may be null if there are no results- hasPreviousPage: Non-null boolean that indicates if there are more edges before the current page
last/before
:true
if prior edges existfirst/after
: optionallytrue
if edges beforeafter
exist
- hasNextPage: Non-null boolean that indicates if there are more edges after the current page
first/after
:true
if more edges existlast/before
: optionallytrue
if edges afterbefore
exist
- startCursor:Cursors of first and last nodes in edges. Opaque strings (nullable if no results)
- endCursor: Cursors of first and last nodes in edges. Opaque strings (nullable if no results)
- hasPreviousPage: Non-null boolean that indicates if there are more edges before the current page
- Gateway vs Router
- Federation Versions
- Graph Types
- Apollo GraphOS
- Namespace
- APQ & PQL
- Federated Entities
- Migration
Aspect | Apollo Gateway | Apollo Router |
---|---|---|
Visualization | ||
Language | Node.js (JavaScript) | Rust |
Latency | Higher latency, especially in scenarios with complex queries | Lower latency due to optimized query execution and multi-threaded design |
Scalability | Scales effectively, but with potential bottlenecks in very high-load scenarios | Highly scalable, leveraging Rust's ability to manage resources efficiently under heavy traffic |
Memory Usage | Comparatively higher memory footprint due to Node.js overhead | Lower memory usage due to Rust's fine-grained memory management |
Customization | High level of customization using JavaScript/Node.js plugins | Customization via Apollo Router extensions, written in Rust or using predefined features |
Federation Version Compatibility | Apollo Federation 1 and 2 | Optimized for Apollo Federation 2, but also compatible with Federation 1 |
Concurrency Model | Single-threaded, event-driven (Node.js) | Multi-threaded by default, leveraging Rust's concurrency model for better utilization of CPU resources |
Resource Efficiency | Moderate resource efficiency, subject to Node.js limitations | High resource efficiency, using Rust's optimized resource management |
Use Case | Ideal for applications with moderate performance needs and developers familiar with JavaScript | Best for high-performance scenarios, large-scale federations, or teams requiring extremely low latency |
Aspect | Apollo Federation Version 1 | Apollo Federation Version 2 |
---|---|---|
Core Architecture | Built on GraphQL schema stitching with a focus on declarative ownership of types across services | Refined architecture with improvements in query planning, security, and extensibility |
Schema Composition | Centralized schema composed using @key , @extends , and other directives | Enhanced schema composition with streamlined support for type extensions and directives |
Directive Support | Core directives: @key , @extends , @external , @provides , @requires | Expanded directive set for better flexibility; supports the same directives with optimized handling |
Supergraph Schema | Not explicitly defined; relies on schema stitching to create a unified API gateway | Introduces the Supergraph Schema, a declarative representation of the federated schema, enhancing visibility and management |
Gateway Implementation | Uses Apollo Gateway to execute federated queries by delegating to subgraphs | Upgraded Apollo Gateway supports Supergraph Schema, advanced query planning, and additional federation features |
Query Planning | Basic query planner that routes subqueries based on ownership of types and fields | Enhanced query planner with more efficient routing, reduced network overhead, and support for shared ownership scenarios |
Type Ownership | Types are owned by individual subgraphs; conflicts resolved during composition | Improved support for shared ownership, providing more granular control over type definitions |
Error Handling | Errors are propagated to the client with limited granularity | Improved error propagation with clearer differentiation between subgraph-level and gateway-level issues |
Security Enhancements | Basic security measures, such as authentication and authorization at the gateway level | Built-in authz and authn hooks, improved validation of schema boundaries, and enhanced cross-subgraph communication security |
Performance | Good performance, but query planning could introduce noticeable overhead for complex schemas | Significant performance improvements due to an optimized query planner and reduced round-trip overheads |
Backward Compatibility | Not forward-compatible with Apollo Federation 2 | Backward-compatible with Federation 1 schemas; includes a migration path for upgrading |
Subgraph Support | Basic support for subgraph APIs with ownership and dependency definitions | More flexible subgraph support, including enhanced debugging, versioning, and validation tools |
Multi-Version Subgraph Support | Limited support; requires careful schema management | Full multi-version support, making it easier to manage versioned APIs across subgraphs |
Aspect | Monograph | Supergraph |
---|---|---|
Visualization | ||
Definition | Graph that represents a single domain, team, or service | Federated graph that unifies multiple monographs under a single API schema |
Architecture Style | Typically decentralized, focusing on individual domain responsibilities | Centralized or federated with a unified schema and gateway |
Scalability | Scales horizontally within the specific domain | Scales across domains by delegating queries to appropriate subgraphs |
Implementation Complexity | Relatively simple; involves defining schemas and resolvers for one domain | High; involves federation, schema stitching, and cross-team collaboration |
Data Ownership | Clear ownership, as each graph is tied to a single team or service | Ownership is distributed across teams but unified under shared governance |
Team Collaboration | Limited to the domain team | Requires cross-domain collaboration and clear ownership agreements |
Query Execution | Queries are resolved locally within the monograph | Queries are resolved across multiple subgraphs and federated |
Use Cases | Isolated domains (e.g., User Management, Inventory, Orders) | Enterprise-wide APIs (e.g., unified e-commerce, connected healthcare) |
While the current approach works in a GraphQL server, it fails to meet the spec requirement that field resolutions, except for top-level mutations, must be side effect-free and idempotent. It is recommended that GraphQL mutations be defined at the root level for serial execution in line with the specification. Currently, the only way to group related mutations is through field naming conventions and careful ordering, as there is no spec-compliant solution for managing numerous fields on the root mutation type
Query: Root-Level Operation Fields
# Define all query fields for User objects in a UsersQueries namespace
type UsersQueries {
all: [User!]!
}
# Define all query fields for Comment objects in a CommentsQueries namespace
type CommentsQueries {
byUser(user: ID!): [Comment!]!
}
# Add a single root-level namespace-type which wraps other queries
type Query {
users: UsersQueries!
comments: CommentsQueries!
}
# Fetch all users
query FetchAllUsers {
users {
all {
id
firstName
lastName
}
}
}
Mutation: Root-Level Operation Fields
# Define all mutation fields for User objects in a UsersMutations namespace
type UsersMutations {
create(profile: UserProfileInput!): User!
block(id: ID!): User!
}
# Define all mutation fields for Comment objects in a CommentsMutations namespace
type CommentsMutations {
create(comment: CommentInput!): Comment!
delete(id: ID!): Comment!
}
# Add a single root-level namespace-type which wraps other mutations
type Mutation {
users: UsersMutations!
comments: CommentsMutations!
}
# Create a new user and return it
mutation CreateNewUser($userProfile: UserProfileInput!) {
users {
create(profile: $userProfile) {
id
firstName
lastName
}
}
}
Serial Mutations
Root-level Mutation fields must be resolved serially to prevent simultaneous interactions with the same data, avoiding race conditions
mutation Transaction {
user {
success
}
# `payment` field resolves only after `user`,
# and won't resolve if `user` encounters an error
payment {
success
}
}
With namespaces, mutation fields that modify data are no longer root-level fields, allowing them to be resolved in parallel. To ensure transactional consistency use saga orchestrator in your mutation resolvers
mutation ParallelMutations (
$createInput: CreateReviewInput!
$deleteInput: DeleteReviewInput!
) {
reviews {
create(input: $createInput) {
success
}
# resolved in parallel with `create`
delete(input: $deleteInput) {
success
}
}
}
To ensure serial execution in a specific operation, you can use client-side aliases to create two root fields that resolve serially
mutation SerialMutations(
$createInput: CreateReviewInput!
$deleteInput: DeleteReviewInput!
) {
createReviews: reviews {
create(input: $createInput) {
success
}
}
# resolved serially after `createReviews` is resolved
deletedReviews: reviews {
delete(input: $deleteInput) {
success
}
}
}
Aspect | Automatic Persisted Query (APQ) | Registered Persisted Query (RPQ) |
---|---|---|
Visualization | ||
Definition | Query strings cached on the server side with a unique identifier (SHA-256 hash) | Pre-registered queries stored in a persisted query list (PQL) and identified by a unique ID |
Performance | Reduces request sizes by sending identifiers instead of full query strings | Shares APQ's performance benefits and includes query plan cache warm-ups for even faster performance |
Registration | Queries are registered at runtime; the server must receive the query string at least once | Queries are registered at build-time, allowing immediate execution using their PQL-specified ID |
Safelisting | Does not provide safelisting capabilities as the cache is populated dynamically | Enables safelisting by rejecting operations not present in the PQL, enhancing security |
Flexibility | Offers more flexibility as new queries can be added on the fly | Less flexible as changes to queries require updating the PQL at build time |
Security | Less secure as it accepts any query that is sent at least once | More secure as it only accepts pre-registered queries, preventing malicious requests |
Use Case | Suitable for improving network performance for large query strings without additional security concerns | Ideal for applications that require both performance optimization and enhanced security measures |
Entity: Resolves fields across multiple subgraphs, where each subgraph contributes unique fields and resolves only its own; only object types can be entities, and each contributing subgraph must define a reference resolver
Keys
- Keys (best to use non-nullable fields) must uniquely identify the entity and can be a combination of one or more fields
- Cannot include fields that return a union, interface, or fields that take arguments
# Products Subgraph
type Product @key(fields: "upc") {
upc: ID!
name: String!
price: Int
}
# Reviews Subgraph
type Product @key(fields: "productUpc") {
productUpc: ID!
rating: Int!
}
Key Variants
Aspect | Definition | Example |
---|---|---|
Compound @keys | Combine multiple fields to uniquely identify an entity |
|
Nested Fields in Compound @keys | Nested fields can be used in compound keys |
|
Multiple @keys | Define multiple @keys when different subgraphs interact with different fields |
|
Referencing Entities with Multiple Keys | Subgraphs can reference an entity using any @key fields |
|
Differing @keys Across Subgraphs | Different subgraphs can use different @keys for the same entity |
|
Merging Entities | Entities must share at least one field in their @key selection set to merge |
|
Operations with Differing @keys | Differing keys affect which fields can be resolved from each subgraph |
|
Query Resolution | Queries in the Products subgraph can resolve all product fields, while queries in the Inventory subgraph can only resolve fields from the Products subgraph if both share a key |
|
Contribute Fields
Subgraphs in a federated GraphQL architecture can contribute to and reference fields from shared entities.
Router Processing of Computed Fields: When a query requests a computed field, the router queries the relevant subgraph for required fields, then for the computed field, and finally passes the required fields to the resolver.
Aspect | Definition | Example |
---|---|---|
Contributing Entity Fields | Multiple subgraphs can contribute different fields to an entity |
|
Contributing Computed Entity Fields | Subgraphs can define fields computed from other entity fields |
|
sing @requires with Object Subfields | Specify subfields required from an object type |
|
Using @requires with Fields that Take Arguments | @requires can include fields with arguments |
|
Referencing an Entity Without Contributing Fields | Subgraphs can reference an entity without contributing fields |
|
Directive | Description | Example |
---|---|---|
@shareable | Allows multiple subgraphs to resolve a field; if a field is marked as @shareable in any subgraph, it must also be marked as @shareable or @external in all subgraphs that define it |
|
@provides | Indicates that a field can be resolved by a subgraph at a specific query path Rules:
|
|
Entity Interfaces
Rules:
- Interface Definition
- Must include at least one
@key
directive - Subgraph that owns all entities implementing the interface
- Must include at least one
@interfaceObject
Definitions- Must reference an existing interface with
@key
- Cannot define the interface as an object type in the same subgraph
- Must reference an existing interface with
- Resolvers
- Reference Resolver: Needed for entity interfaces
- Field Resolvers: Required for new fields in object types
@interfaceObject
- Prevents maintenance issues by allowing subgraphs to generically add fields without needing to redefine entities
- Avoids composition errors when new entities are added
Migrate entity or root fields from one subgraph to another as the supergraph evolves
|
|
Migration Process with @override
- Import @override Directive
extend schema
@link(url: "https://specs.apollo.dev/federation/v2.7",
import: ["@key", "@shareable", "@override"])
- Update Billing Subgraph. Add the
amount
field with@override
type Bill @key(fields: "id") {
id: ID!
amount: Int! @override(from: "Payments")
}
-
Publish Updated Billing Schema. The router resolves
Bill.amount
from the Billing subgraph while still resolvingBill.payment
from Payments -
Remove Field from Payments Subgraph
type Bill @key(fields: "id") {
id: ID!
payment: Payment
}
- Update Billing Subgraph. Remove the
@override
directive
type Bill @key(fields: "id") {
id: ID!
amount: Int!
}
- Core Principles
- Performance
- Nesting in GraphQL Schemas
- Design Principles
- Security
- Federation
- Use Clear Naming Conventions: adopt consistent naming conventions like
camelCase
for fields andPascalCase
for types to make the schema self-documenting and easy to follow - Keep the Schema Simple: start with a basic schema and evolve it as needed. Over-engineering from the start can lead to unnecessary complexity
- Utilize Interfaces and Unions: these allow for the representation of shared features and the combination of different types, providing flexibility in the schema design
- Versioning: GraphQL takes a strong opinion on avoiding versioning by providing the tools for the continuous evolution of a GraphQL schema. This means new capabilities can be added without creating breaking changes
- Nullability: properly use nullability to indicate whether a field can be null. This helps with the predictability and reliability of the API responses
- Abstract Types: Use abstract types like interfaces and unions to model shared characteristics between entities
- Caching: Implement caching mechanisms for frequently accessed data to reduce server load and improve response times
- Object Types: Define object types to represent data entities with well-defined properties
- Input Object Types: Use input object types to structure data for mutations
- Complex Data Structures: Define object types for complex data structures
- Custom Scalars: Define custom scalar types for specialized data formats to improve type safety and expressiveness
- Entity Identification: Ensure each entity has a unique identifier, typically an ID field
- Enums: Employ enums for defining finite sets of possible values
- Field-Level Resolution: Implement field-level resolvers to optimize data fetching and avoid over-fetching unnecessary data
- GraphQL Variables: Use GraphQL variables to avoid query string manipulation to improve cache effectiveness and information privacy
- Type Extensions: Utilize GraphQL type extensions to add additional fields or functionality to existing types without modifying their original definition
- Language Agnostic: The schema should be independent of programming language
- Batching: Consider server-side batching to improve performance by fetching related data in a single request
- Data Pagination: Implement cursor-based pagination for large datasets to improve query performance and reduce memory overhead
- Dynamic Discovery: Consider using a registry or service mesh for dynamic discovery
- Optimized Resolvers: Write efficient resolver functions to minimize data fetching latency and optimize query execution
- Query Complexity Analysis: Perform query complexity analysis to prevent costly or inefficient queries from impacting system performance
- Limit Depth of Nesting: deeply nested queries can lead to performance issues. It's important to limit the depth of nesting to what is necessary for the application's functionality
- Use Batch Loading: to prevent the N+1 problem, where multiple nested queries cause a cascade of database calls, use batch loading techniques like DataLoader to batch requests to the database
- Designing for Clarity: when nesting, ensure that the structure is clear and logical. The relationships between types should be intuitive, allowing developers to easily understand the schema and build queries
- Federated Schemas: for complex schemas it can be used to combine multiple schemas into one. This allows for a modular approach to schema design, where each module can be developed independently and then stitched together
- Schema Definition Language (SDL): Utilize GraphQL SDL (Schema Definition Language) to define types, queries, mutations, and subscriptions with clear and concise naming conventions
- Flat Structure Preference: minimize deep nesting by promoting a flat schema structure where possible. This reduces complexity and improves performance
- Simplicity: Schema should be designed with both simplicity and flexibility in mind. It should be easy to understand and navigate, avoiding overly complex or deeply nested structures that can become difficult to maintain
- Entity Normalization: break down complex entities into smaller, normalized types to allow reuse and easier resolution. Use references (IDs) and separate queries for resolving deeply nested data
- Pagination and Limits: implement pagination (edges and nodes) for nested lists to prevent overwhelming the server
- Avoid N+1 Problem: design resolvers with batching mechanisms (e.g.,
DataLoader
) to mitigate N+1 query issues in nested relationships - Error Handling and Partial Responses: ensure the schema can handle errors gracefully, especially in nested cases, by leveraging GraphQL's error format for partial responses
- Client-Driven Design: Focus on the specific needs of client applications. Prioritize functionality used by multiple clients, but avoid bloating the schema with features for a single client
- Demand-Oriented: Add functionalities gradually based on client requirements to lean and efficient schema
- Evolvable Schema: Design for future growth. Consider potential use cases and data needs that may arise
- Demand Oriented (abstract): Design demand-oriented schemas by shifting to a common graph for simplified client data access via GraphQL, ensuring schemas are abstract and not tightly coupled to specific clients or services, despite GraphQL's client-driven design not guaranteeing usability
- Prioritize Client Needs: Consult client teams early in the API design process, conducting ongoing research to adapt to evolving requirements, and engaging them in defining data needs and ideal data shapes
- Avoid Service Implementation Details: Prevent schema design from being influenced by backing services, using federation to express natural relationships between types, and refraining from exposing unnecessary fields and implementation details in the schema
- Enhance Schema Expressiveness: Convey meaning about nodes and relationships, standardizing naming and formatting conventions across services, and ensuring consistent pagination experiences throughout the graph
- Design Fields for Specific Use Cases: Create single-purpose fields for clarity, utilizing finer-grained mutations and queries to avoid ambiguity, and thoroughly documenting types, fields, and arguments for transparency
- Document Schema Effectively: Treat documentation as a first-class feature in GraphQL, using SDL-supported description syntax for clarity, and establishing standards and governance for consistency
- Transport Layer Security: Implement TLS (Transport Layer Security) for secure communication over the network
- Authentication and Authorization: Implement authentication and authorization mechanisms to ensure secure access to the GraphQL API
- Depth limiting: Restrict the maximum number of nested levels in queries. It helps prevent denial-of-service (DoS) attacks and excessive resource consumption by rejecting overly complex queries, ensuring better performance and resource management for the server
- Breadth Limiting restricts the number of fields requested at a single level in a GraphQL query
- Batch Limiting controls the number of operations sent in a single request to prevent server overload
- Rate Limiting: Implement rate limiting to prevent abuse and ensure proper usage of the GraphQL API
- Error Handling: Define a consistent error handling strategy to provide meaningful error messages to clients
- Automatic Persisted Queries (APQ): Use APQ to improve query performance by caching queries and responses
- Registered Persisted Query (RPQ): Accepts pre-registered queries, preventing malicious requests
- Federated Supergraph: Leverage tools like Apollo Router for simplified federation setup and management
- Federation: Align subgraphs with business domains for ownership and maintainability
- Gateway Configuration: Use GraphQL Gateway to orchestrate requests across federated services
- Modular Schema: Encourage a modular approach to schema design, where each domain or microservice defines its own GraphQL schema
- Schema Stitching: Foster collaboration between teams to align on a unified graph schema that reflects the entire organization's data model
- Service Configuration: Configure each service to participate in the federation, including endpoint URLs and schemas
- Service Discovery: Implement a robust service discovery mechanism for the federation gateway to locate subgraphs
- Service Implementation: Develop individual GraphQL services responsible for serving specific types or subsets of types
- Subgraphs: Define clear boundaries between subgraphs to avoid overlap and conflicts
- Type Definitions: Define entity types and their relationships in each service's GraphQL schema
@external
Directive: Mark fields from other services as external using@external
directive@key
Directive: Annotate entity fields with@key
directive to declare them as federation keys@requires
and@provides
Directives: Specify field dependencies using these directives to enable automatic query planning