Graphql
💒

Graphql

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.
GraphQL
query { 
  viewer {
    login
    isHireable
    name
    location   
    status {
      message
      emoji
      expiresAt
    }
  }
}

Parameters in queries

GraphQL
query { 
  repository(owner: "apollographql", name:"apollo-server") {
    # List fields to get here
    ...
  }
}

Use parameters

GraphQL
query($authorName: String!, $categoryId: String!) {
  ...
}

Multiple queries

GraphQL
query {
  viewer {
    login
    isHireable
    name
    location   
  }
  repository(owner: "apollographql", name:"apollo-server") {
    name
    forkCount
    owner {
      login
    }
  }
}
 
GraphQL
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

GraphQL
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:
JavaScript
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:
JavaScript
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:
JavaScript
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:
JavaScript
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:
JavaScript
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:
JavaScript
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

JavaScript
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
GraphQL
type Author {
  id: Int!
  firstName: String
  lastName: String
  posts: [Post!]!
}