GraphQL works by defining a schema that describes the data available through the API. The schema specifies the types of objects available, as well as the fields on those objects that can be queried. Clients can then use the schema to construct queries that specify exactly what data they need.
When a client sends a query to the server, GraphQL parses the query and executes it against the schema. The server then returns only the data requested in the query, in the format specified by the client. This makes API interactions more efficient and reduces the amount of unnecessary data transferred between client and server.
query { viewer { login isHireable name location status { message emoji expiresAt } } }
Parameters in queries
query { repository(owner: "apollographql", name:"apollo-server") { # List fields to get here ... } }
Use parameters
query($authorName: String!, $categoryId: String!) { ... }
Multiple queries
query { viewer { login isHireable name location } repository(owner: "apollographql", name:"apollo-server") { name forkCount owner { login } } }
Â
fragment RepositorySummary on Repository { name forkCount primaryLanguage { name } } query { apolloServer: repository(owner: "apollographql", name: "apollo-server") { ...RepositorySummary } apolloIos: repository(owner: "apollographql", name: "apollo-ios") { ...RepositorySummary } }
Â
Mutation
type Mutation { login(userName: String!, password: String!): LogInResponse! logOut: Boolean! } type LogInResponse { expiresIn: Int! user: User! }
Â
Here's an example of using DataLoader with Apollo Server:
const { ApolloServer } = require('apollo-server'); const DataLoader = require('dataloader'); // Define the DataLoader function const batchUsers = async (keys) => { const users = await User.find({ _id: { $in: keys } }); return keys.map((key) => users.find((user) => user.id === key)); }; // Create a new DataLoader instance const userLoader = new DataLoader((keys) => batchUsers(keys)); // Define the resolvers const resolvers = { Query: { user: async (parent, { id }, { loaders }) => { return loaders.userLoader.load(id); }, }, }; // Create the Apollo Server instance const server = new ApolloServer({ typeDefs, resolvers, context: () => ({ loaders: { userLoader, }, }), }); // Start the server server.listen().then(({ url }) => { console.log(`Server ready at ${url}`); });
In this example, we define a DataLoader function
batchUsers
that retrieves an array of users from the database based on an array of IDs. We then create a new DataLoader instance userLoader
using batchUsers
. In the Query resolver for the user
field, we use loaders.userLoader.load(id)
to retrieve a single user from the database using the DataLoader instance. By using DataLoader, we can batch queries for users together, reducing the number of database queries and improving the performance of our API.Â
Apollo Client Cache
Apollo Client includes a built-in cache that can be used to store and manage data received from the server. The cache can be used to reduce the number of network requests made by the client, by allowing the client to reuse data that has already been fetched.
When a query is executed using Apollo Client, the cache is checked to see if the query has already been executed before. If the query has been executed before, and the data in the cache has not expired, the data is returned from the cache instead of being fetched from the server. If the query has not been executed before, or the data in the cache has expired, the query is executed against the server.
The cache can be configured to store data for a specific amount of time, or to store data indefinitely. The cache can also be configured to automatically evict data from the cache when it becomes stale or when the cache becomes too large.
In addition to caching data, Apollo Client also includes a number of utilities for working with cached data. These utilities can be used to read data from the cache, write data to the cache, and update data in the cache.
Here's an example of using the
useQuery
hook in Apollo Client to fetch data from the server and store it in the cache:import { useQuery, gql } from '@apollo/client'; const GET_BOOKS = gql` query GetBooks { books { title author } } `; function BookList() { const { loading, error, data } = useQuery(GET_BOOKS); if (loading) return <p>Loading...</p>; if (error) return <p>Error :(</p>; return data.books.map(({ title, author }) => ( <div key={title}> <p>{title} by {author}</p> </div> )); }
In this example, we define a query
GET_BOOKS
that retrieves a list of books from the server. We then use the useQuery
hook to execute the query and store the results in the cache. If the query has already been executed before, and the data is still valid in the cache, the cached data is returned instead of being fetched from the server.By using the cache in this way, we can reduce the number of network requests made by our application, and improve the performance and responsiveness of our user interface.
InMemoryCache
The
InMemoryCache
is the default cache implementation used by Apollo Client. It is a normalized, in-memory cache that stores data in a normalized form, using a unique identifier to reference each object. When data is fetched from the server, it is normalized and stored in the cache, allowing it to be easily accessed and updated later.The
InMemoryCache
can be configured to store data for a specific amount of time, or to store data indefinitely. It can also be configured to automatically evict data from the cache when it becomes stale or when the cache becomes too large.To configure the
InMemoryCache
, you can pass a configuration object to the ApolloClient
constructor, like so:import { ApolloClient, InMemoryCache } from '@apollo/client'; const client = new ApolloClient({ uri: '<https://my-graphql-server.com/graphql>', cache: new InMemoryCache({ typePolicies: { Query: { fields: { books: { keyArgs: false, merge(existing, incoming) { return incoming; }, }, }, }, }, }), });
In this example, we create a new
ApolloClient
instance and pass it a new instance of the InMemoryCache
. We also pass a typePolicies
configuration object to the InMemoryCache
constructor, which defines how data should be stored and merged in the cache.In the
typePolicies
object, we define a policy for the books
field on the Query
type. We set keyArgs
to false
, which tells Apollo Client to use the entire query as the cache key for the books
field. We also define a merge
function that tells Apollo Client how to merge data from the server with data in the cache.By using the
InMemoryCache
, we can take advantage of Apollo Client's caching capabilities to reduce the number of network requests made by our application and improve the performance of our user interface.Â
optimisticResponse
The
optimisticResponse
option in Apollo Client allows you to specify a response that should be immediately returned by the client when a mutation is executed, before the server has actually responded. This can be useful for providing immediate feedback to the user, even if the server response is delayed or the mutation fails.Here's an example of using
optimisticResponse
with a mutation in Apollo Client:import { useMutation, gql } from '@apollo/client'; const ADD_TODO = gql` mutation AddTodo($text: String!) { addTodo(text: $text) { id text completed } } `; function TodoForm() { const [text, setText] = useState(''); const [addTodo, { loading }] = useMutation(ADD_TODO, { optimisticResponse: { __typename: 'Mutation', addTodo: { __typename: 'Todo', id: uuid(), text, completed: false, }, }, update(cache, { data: { addTodo } }) { cache.modify({ fields: { todos(existingTodos = []) { const newTodoRef = cache.writeFragment({ data: addTodo, fragment: gql` fragment NewTodo on Todo { id text completed } `, }); return [...existingTodos, newTodoRef]; }, }, }); }, }); const handleSubmit = (event) => { event.preventDefault(); addTodo({ variables: { text }, }); setText(''); }; return ( <form onSubmit={handleSubmit}> <input type="text" placeholder="Enter a new todo" value={text} onChange={(event) => setText(event.target.value)} disabled={loading} /> <button type="submit" disabled={loading}> Add Todo </button> </form> ); }
In this example, we define a mutation
ADD_TODO
that adds a new todo to a list of todos on the server. We then use the useMutation
hook to execute the mutation and specify an optimisticResponse
that immediately adds the new todo to the client-side cache, before the server has actually responded.By providing an
optimisticResponse
, we can provide immediate feedback to the user that their todo has been added, even if the server response is delayed or the mutation fails. If the mutation is successful, the server response will be used to update the client-side cache. If the mutation fails, the optimistic response will be rolled back and the client-side cache will be updated to reflect the failed mutation.By using
optimisticResponse
in this way, we can improve the perceived performance of our application and provide a better user experience.fetchMore
fetchMore
is a function provided by Apollo Client that allows you to fetch additional data from the server and merge it into the existing data in the cache. This can be useful for implementing infinite scrolling, pagination, or other similar features.Here's an example of using
fetchMore
to load additional data from the server:import { useQuery, gql } from '@apollo/client'; const GET_BOOKS = gql` query GetBooks($cursor: String) { books(after: $cursor) { edges { node { id title author } } pageInfo { hasNextPage endCursor } } } `; function BookList() { const { loading, error, data, fetchMore } = useQuery(GET_BOOKS); if (loading) return <p>Loading...</p>; if (error) return <p>Error :(</p>; return ( <div> {data.books.edges.map(({ node }) => ( <div key={node.id}> <p>{node.title} by {node.author}</p> </div> ))} {data.books.pageInfo.hasNextPage && ( <button onClick={() => fetchMore({ variables: { cursor: data.books.pageInfo.endCursor, }, }) } > Load More </button> )} </div> ); }
In this example, we define a query
GET_BOOKS
that retrieves a list of books from the server. We use the useQuery
hook to execute the query and store the results in the cache. We also pass the fetchMore
function to the component, which can be used to load additional data from the server.When the user clicks the "Load More" button, we call the
fetchMore
function and pass it a variables
object that includes the endCursor
value from the pageInfo
object in the data
returned by the previous query. This tells the server to return the next page of data, starting from the endCursor
.When the new data is returned from the server, Apollo Client merges it into the existing data in the cache, and the component re-renders with the additional data included.
By using
fetchMore
in this way, we can implement infinite scrolling or pagination in our application, and provide a better user experience by loading data on demand instead of all at once.Â
- Use the
merge
function to tell Apollo Client how to merge data from the server with data in the cache.
- Use
fetchMore
to load additional data from the server and merge it into the existing data in the cache.
- Use the cache to reduce the number of network requests made by the client, by allowing the client to reuse data that has already been fetched.
Â
MockedProvider
To use
MockedProvider
to test React components, you can import it from @apollo/client/testing
.Here's an example of how to use
MockedProvider
to test a component that uses the useQuery
hook from Apollo Client:import { render, screen, waitFor } from '@testing-library/react'; import { MockedProvider } from '@apollo/client/testing'; import { GET_BOOKS } from './queries'; import BookList from './BookList'; const mocks = [ { request: { query: GET_BOOKS, }, result: { data: { books: [ { id: '1', title: 'The Great Gatsby', author: 'F. Scott Fitzgerald' }, { id: '2', title: 'To Kill a Mockingbird', author: 'Harper Lee' }, { id: '3', title: 'Pride and Prejudice', author: 'Jane Austen' }, ], }, }, }, ]; describe('BookList', () => { it('renders a list of books', async () => { render( <MockedProvider mocks={mocks} addTypename={false}> <BookList /> </MockedProvider> ); await waitFor(() => { expect(screen.getByText('The Great Gatsby by F. Scott Fitzgerald')).toBeInTheDocument(); expect(screen.getByText('To Kill a Mockingbird by Harper Lee')).toBeInTheDocument(); expect(screen.getByText('Pride and Prejudice by Jane Austen')).toBeInTheDocument(); }); }); });
In this example, we define an array of
mocks
that specify the data that the component will receive from the server. We then render the component using MockedProvider
and pass it the mocks
array.We use the
waitFor
function from @testing-library/react
to wait for the component to finish rendering before making our assertions. We then use screen.getByText
to check that the expected book titles and authors are present in the rendered component.By using
MockedProvider
in this way, we can test our components in isolation from the rest of our application, and ensure that they behave correctly in response to the data returned by our API.Note that we set
addTypename
to false
when rendering the component with MockedProvider
. This is because MockedProvider
automatically adds __typename
fields to the data returned by the mock queries, which can cause issues when testing. By setting addTypename
to false
, we can disable this behavior and ensure that our tests behave predictably.Â
Â
Nestjs
import { Field, Int, ObjectType } from '@nestjs/graphql'; import { Post } from './post'; @ObjectType() export class Author { //it is required for number (which must be mapped // to either a GraphQL Int or Float). @Field(type => Int, { nullable: false }) id: number; /* nullable: for specifying whether a field is nullable (in SDL, each field is non-nullable by default); boolean description: for setting a field description; string deprecationReason: for marking a field as deprecated; string */ @Field({ nullable: true }) firstName?: string; @Field({ nullable: true }) lastName?: string; // When the field is an array, we must // manually indicate the array type in the Field() decorator's type function @Field(type => [Post]) posts: Post[]; }
will generate
type Author { id: Int! firstName: String lastName: String posts: [Post!]! }