What is GraphQL?
A GraphQL server |
Key Features of GraphQL
- Single Endpoint: All requests are sent to a single endpoint.
- Strongly Typed: A schema defines the data structure and enforces types.
- Declarative Queries: Clients specify the shape of the response they need.
- Real-time Support: Through subscriptions, clients can receive real-time updates.
- Introspection: Enables clients to query the schema for metadata.
Architecture and Use Cases
GraphQL can be integrated into various architectural patterns, including:- Backend for Frontend (BFF):
- Ideal for mobile or web applications requiring tailored data responses.
- Simplifies aggregating data from multiple sources.
- Monolith Migration:
- Gradual replacement of REST endpoints in a monolithic application.
- Reduces disruption while adopting GraphQL incrementally.
- Microservices:
- Federated GraphQL architecture enables seamless integration of microservices.
- A gateway aggregates and resolves queries across multiple subgraphs.
Drawbacks
While GraphQL is powerful, it’s not without challenges:- Steep learning curve: Unlike REST, GraphQL requires a strong understanding of concepts like schemas, resolvers, and federation.
- Complexity in high-level composition: Queries across subgraphs with dependencies are not automatically resolved. Manual invocations are often necessary.
- Performance concerns: Misusing resolvers or failing to batch queries can lead to performance bottlenecks.
- Complex schema: It is hard to track when multiple subgraphs contributing to the same type.
- Complex resolvers: It is hard to understand data resolving when the chain is deep.
What Does GraphQL Look Like?
Imagine a scenario where we need to fetch books by their id, title, and price. Here’s how a GraphQL client query and its corresponding server schema might look:query GetBooks {
books {
id
title
price
}
}
type Query {
books: [Book]
}
type Book {
id: ID
title: String
price: Float
genre: [Genre]
}
GraphQL Concepts
Understanding these core concepts is crucial when working with GraphQL:- Schema: Defines the types, queries, and mutations that the API supports.
- Types: GraphQL objects define the shape of the API’s data. Common types include:
- Root types
- Query - Read-only operation to fetch data.
- Mutation - Used to modify server-side data.
- Subscription - Real-time updates
- Built-in scalars - ID, String, Int, Float, Boolean
- Custom types - Book, Author - type Book {}
- Input types - Defines reusable input objects for arguments in queries/mutations. input BookInput { title: String! authorId: ID! }
- Enums - Defines a fixed set of values for a field. - enum Genre {}
- Custom scalars - Define custom types like Date or Email. scalar Date
- Resolver: A function responsible for fetching the data for a specific field in the schema.
- Resolver chain - the sequential execution of resolvers to fetch nested or related data for a query.
- Directives: Annotations for modifying query behavior. Examples:
- @deprecated(reason: "Use 'newField' instead.")
- @include and @skip for conditional fields.
- @key and @external for federation.
- Custom directives for additional logic.
- Arguments: Used to pass data to fields, enabling dynamic responses.
- Parent: The result of the previous resolver in the chain. Useful for nested data.
- Context: Provides a shared object across all resolvers during a query.
- DataLoader: Solves the N+1 query problem by batching and caching requests.
- Extend: Adds fields to existing types across subgraphs in federated GraphQL. extend type Author
- Federation: A way to compose multiple GraphQL APIs into a single graph. Key concepts include:
- @key - Declares a primary key field in the schema to uniquely identify types.
- @external - Marks a field as resolved by another subgraph.
- @requires - Specifies dependent fields needed to resolve another field.
- @provides - Indicates that a resolver provides additional data for another subgraph.
- __resolveReference - Resolver method to fetch type instances by key from another subgraph.
- __entities - Federated query for resolving multiple references across subgraphs in a single query. Automatically generated for federated operations.
- __typename - the name of the object type being resolved in a GraphQL query.
- @shareable Allows a field to be shared across multiple subgraphs.
- @override Specifies which subgraph should resolve a field when it’s available in multiple subgraphs.
- _service - query that provides the subgraph's SDL. _service { sdl }
- Fragments: Reusable parts of queries to avoid redundancy. fragment BookDetails on Book { title author { name } }
- Introspection - Queries about the schema itself. { __schema { types { name } } }
How Does the Gateway Work?
In a federated setup, the gateway acts as the orchestrator.- Aggregates schemas from multiple subgraphs into a single schema.
- Resolves queries by delegating them to the appropriate subgraph.
- Uses _entities and _service queries to fetch federated data efficiently.
query Book_entities($representations: [_Any!]!) {
_entities(representations: $representations) {
... on Book {
id
title
authorId
}
}
}
// pass the parameters
{
"representations": [
{ "__typename": "Book", "id": "1" }
]
}
How Does the Gateway Resolve Subgraph Dependencies?
The gateway uses @key and @external directives to link entities across subgraphs. For example, if the Books subgraph has an author and the Authors subgraph provides details about authors, the gateway combines them to resolve a complete query response.- Request books, book has author reference.
- Gateway fetches books from book-subgraph.
- Books subgraph returns books and author reference.
- Gateway invoke author-subgraph's __entities query using author references.
- __entities query invoke __resolveReference in a loop.
- __resolveReference resolve author entities, uses dataloader for peroformance.
- Gateway returns books with author entities.
Standalone Server
As mentioned earlier, we can use GraphQL as a monolithic gateway. In this setup, there is no need to install a dedicated gateway or subgraph dependencies.npm install @apollo/server graphql graphql-tag dataloader express body-parser cors
const { ApolloServer } = require('@apollo/server'); const { expressMiddleware } = require('@apollo/server/express4'); const express = require('express'); const bodyParser = require('body-parser'); const cors = require('cors'); // import schema and resolvers const typeDefs = require('./schema'); const resolvers = require('./resolvers'); const authorLoader = require('./data-loaders'); // Define context function const context = async ({ _req, _res }) => { return { loaders: { authorLoader }, }; }; // Create an instance of ApolloServer const server = new ApolloServer({ typeDefs, resolvers, }); // Start the server const startServer = async () => { await server.start(); const app = express(); app.use(cors()); app.use(bodyParser.json()); app.use('/graphql', expressMiddleware(server, { context // pass the context })); const PORT = 4000; app.listen(PORT, () => { console.log(`🚀 Server ready at http://localhost:${PORT}/graphql`); }); }; startServer();
Federated Server
In the federated server, we have separated microservices and a gateway to combine them together. We are creating two microservices for books and authors: the books subgraph, the authors subgraph, and the federated gateway.book-shop
---book-shop-gateway
---books-subgraph
---authors-subgraph
Install dependencies
Subgraphs: Navigate to the respective folders (books-subgraph, authors-subgraph) and install the dependencies.npm install @apollo/server graphql @apollo/subgraph express graphql-tag dataloader
npm install @apollo/server graphql @apollo/gateway express
Resolvers
Query: {
searchBook: (parent, args, context, info) => {
// search books from a database based on args/parent
return books;
}
}
Author subgraph
const { ApolloServer } = require('@apollo/server');
const { buildSubgraphSchema } = require('@apollo/subgraph');
const { startStandaloneServer } = require('@apollo/server/standalone');
const { gql } = require('graphql-tag');
const DataLoader = require('dataloader');
// author data
const authors = [
{ id: "1", name: "Author 1" },
{ id: "2", name: "Author 2" },
{ id: "3", name: "Author 3" },
];
// author dataloader
const authorLoader = new DataLoader(async (authorIds) => {
const authorsMap = new Map(authors.map((author) => [author.id, author]));
return authorIds.map((id) => authorsMap.get(id) || null);
}, {
cache: false,
});
// schema
const typeDefs = gql`
type Query {
authors: [Author]
}
type Author @key(fields: "id") {
id: ID!
name: String!
}
`;
// resovlers
const resolvers = {
Query: {
authors: () => authors,
},
Author: {
__resolveReference(author, { authorLoader }) {
return authorLoader.load(author.id);
},
},
};
// start the server
const server = new ApolloServer({
schema: buildSubgraphSchema({ typeDefs, resolvers }),
});
startStandaloneServer(server, {
listen: { port: 4003 },
context: async ({ req }) => {
return {
authorLoader
};
},
}).then(({ url }) => {
console.log(`Authors subgraph is running at ${url}`);
});
query _entities($representations: [_Any!]!) {
_entities(representations: $representations) {
... on Author {
id
name
}
}
}
// variables
{
"representations": [
{ "__typename": "Author", "id": "1" },
{ "__typename": "Author", "id": "2" }
]
}
const { ApolloServer } = require('@apollo/server'); const { buildSubgraphSchema } = require('@apollo/subgraph'); const { startStandaloneServer } = require('@apollo/server/standalone'); const { gql } = require('graphql-tag'); const books = [ { "id": "1", "title": "Book 1", "price": 15.99, "genre": ["Adventure"], "authorId": "1" }, { "id": "2", "title": "Book 2", "price": 12.49, "genre": ["Mystery"], "authorId": "2" }, { "id": "3", "title": "Book 3", "price": 18.75, "genre": ["Biography"], "authorId": "3" }, { "id": "4", "title": "Book 4", "price": 14.99, "genre": ["Adventure", "Mystery"], "authorId": "1" }, { "id": "5", "title": "Book 5", "price": 20.00, "genre": ["Mystery", "Biography"], "authorId": "2" }, ]; const typeDefs = gql` type Query { books: [Book] serachBooks(genre: Genre!): [Book] } type Mutation { addBook(input: AddBookInput!): Book } type Book @key(fields: "id") { id: ID! title: String! price: Float genre: [Genre] author: Author } extend type Author @key(fields: "id") { id: ID! @external } input AddBookInput { title: String! price: Float genre: [Genre]! authorId: ID! } enum Genre { Adventure Biography Mystery } `; const resolvers = { Query: { books: () => books, serachBooks: (_parent, { genre }) => { return books.filter(book => book.genre.includes(genre)); } }, Mutation: { addBook: (_parent, { input }) => { input.id = books.length + 1; books.push(input); return input; } }, Book: { __resolveReference(book) { return books.find(b => b.id === book.id); }, author(book) { return { __typename: "Author", id: book.authorId } } }, }; const server = new ApolloServer({ schema: buildSubgraphSchema({ typeDefs, resolvers }), }); startStandaloneServer(server, { listen: { port: 4001 }, }).then(({ url }) => { console.log(`Books subgraph running at ${url}`); });
Gateway
const { ApolloGateway } = require('@apollo/gateway');
const { ApolloServer } = require('@apollo/server');
const { startStandaloneServer } = require('@apollo/server/standalone');
const gateway = new ApolloGateway({
serviceList: [
{ name: "books", url: "http://localhost:4001" },
{ name: "authors", url: "http://localhost:4003" },
],
});
const server = new ApolloServer({
gateway,
introspection: true,
});
startStandaloneServer(server, {
listen: { port: 4000 },
}).then(({ url }) => {
console.log(`Gateway running at ${url}`);
});
Client query
query Books($genre: Genre!) {
serachBooks(genre: $genre) {
... BookDetails
}
}
mutation Mutation($input: AddBookInput!) {
addBook(input: $input) {
... BookDetails
}
}
fragment BookDetails on Book {
id
title
price
genre
author {
id
name
}
}
// variables
{ "genre": "Adventure", "input": { "title": "Book 6", "price": 28.75, "genre": ["Biography"], "authorId": "3" } }
Conclusion
It has been a great journey with GraphQL so far, with more positives than negatives. However, it's important to understand the core concepts thoroughly before diving in.
Comments
Post a Comment