- Apollo GrpahQL allows a simplified way of building a server with GraphQL. It also allows you do the data binding in various technical stacks using Apollo packages on client side applications.
- GraphQL follows schema first design requiring to think of data in terms of graphs
- In a graph, nodes represent objects, where as arrows (edges) represent relationships
-
A schema defines objects and their properties.
-
It essentially outlines the contract between the server and client through a common interface for data.
-
Here is an example of a partial graphql schema:
type SpaceCat { age: Int missions: [Mission] name: String! }
-
To add description to your schemas, GraphQL allows you to add single comments using
“comment”
(comment between double quotes) and multiline comments using comment between triple double quotes i.e.”””
. -
Following is an example of a schema defining relationships and data query:
const typeDefs = gql` type Query { homeTracks: [Track!] } "Author of a complete Track" type Author { id: ID! "Author's first and last name" name: String! "Author's profile picture url" photo: String } "A track is a group of Modules that teaches about a specific topic" type Track { id: ID! "The track's title" title: String! "The track's main author" author: Author! "The track's main illustration to display in track card or track page detail" thumbnail: String "The track's approximate length to complete, in minutes" length: Int "The number of modules this track contains" modulesCount: Int } `
const { ApolloServer } = require("apollo-server");
const typeDefs = require("./schema");
const server = new ApolloServer({ typeDefs });
server.listen().then(() => {
console.log(`
🚀 Server is running!
🔉 Listening on port 4000
`);
});
To enable mocked data without a backend connection, following example demonstrates the data binding
const { ApolloServer } = require("apollo-server");
const typeDefs = require("./schema");
const mocks = {
Query: () => ({
homeTracks: () => [...new Array(10)],
}),
Track: () => ({
id: () => "track_01",
title: () => "Astro Kitty, Space Explorer",
author: () => {
return {
name: "Grumpy Cat",
photo: "https://res.cloudinary.com/dety84pbu/image/upload/v1606816219/kitty-veyron-sm_mctf3c.jpg",
};
},
thumbnail: () =>
"https://res.cloudinary.com/dety84pbu/image/upload/v1598465568/nebula_cat_djkt9r.jpg",
length: () => 1210,
modulesCount: () => 6,
}),
};
const server = new ApolloServer({ typeDefs, mocks });
server.listen().then(() => {
console.log(`
🚀 Server is running!
🔉 Listening on port 4000
`);
});
import React from "react";
import ReactDOM from "react-dom";
import App from "./app";
import { ApolloProvider, ApolloClient, InMemoryCache } from "@apollo/client";
const client = new ApolloClient({
uri: "http://localhost:4000",
cache: new InMemoryCache(),
});
ReactDOM.render(
<React.StrictMode>
<ApolloProvider client={client}>
<App />
</ApolloProvider>
</React.StrictMode>,
document.getElementById("root")
);
Here, ApolloClient
is used to create a client connected to GraphQL endpoint.
ApolloProvider
accepts the apollo client connected to GraphQL endpoint as a property and allows children to query GraphQL server for data.
InMemoryCache
will cache queried data on client end before performing query to server again.
import React from "react";
import { gql, useQuery } from "@apollo/client";
const TRACKS = gql`
query getTracks {
homeTracks {
id
title
thumbnail
length
modulesCount
author {
id
name
photo
}
}
}
`;
const App = () => {
const { loading, error, data } = useQuery(TRACKS);
if (loading) {
return 'Loading...';
} else if (error) {
return `ERROR: ${error.message}`;
} else {
return JSON.stringify(data);
}
};
export default App;
gql
is used to write the data fetching query. Usually it’s a good idea to query data you need through GraphQL query editor such as GraphiQL or GraphQL playground.TRACKS
in this example holds a single query that we need for fetching data for tracks.getTracks
in query is a placeholder and it can really be anything - however it should be meaningful for your reference to indicate what you are doing with data.homeTracks
is actual GraphQL query on server that will return data.useQuery
is used to fetch data usingTRACKS
query. It returns 3 things:loading
state indicating whether data is still being fetched.error
indicates whether there was an error of not - this is a standard js error object.data
represents actual data returned by server, it can be undefined initially and populated later when data becomes available on client.
- Browser issues query to GraphQL server
- GraphQL server receives request and parses request
- Server builds an AST (Abstract Syntax Tree) to understand the document tree it needs to fetch
- Server then validates the request looking at AST for types and fields defined in schema
- Server will then pass valid request to correct resolver(s) to process it
- Resolver will fetch data / apply mutation and respond with result
- Results will be gathered and sent back to response
-
A resolver is a function whose job is to resolve data for each field given in the query
-
A query consists of fields, each of which uses a resolver to resolve data
-
A resolver function has a standard signature that accepts 4 arguments, i.e.
(parent, args, context, info) => { // gather data return data; }
parent
points to the parent field for which data is being fetched. For e.g. for a Track if author information is being fetched, parent would point to Track entity.args
are actual arguments needed to resolve data. In tracks example for fetching author information, this would beauthorId
to resolve author informationcontext
holds shared data across all resolvers. This contains state information, database connection, authentication state etc.info
contains information about the current graphql operation - this includes field names etc. and could be used to set cache policies at resolver level.
A data source is the mechanism from which you retrieve the data. This can be a database, a REST api, etc.
- Consider following type of structure to simplify organization of resolvers and data sources:
src |- data-sources |-- track-api.js |- resolvers.js
- Each data source should be separately defined in
src/data-sources
- If you're calling a REST api to gather data, you could use
RESTDataSource
fromapollo-datasource-rest
to create an optimized wrapper over your REST api rather than using node-fetch. Reretrieving same data would be much faster withRESTDataSource
extensions. - For resolver functions, if an argument is used, you could use
_
(underscore) to indicate its unused. e.g.(_, __, { dataSource }) => {}
- here you are only using context to accessdataSource
, except that everything else isnt needed.
-
Here is how you could build a wrapper for
TrackApi
usingRESTDataSource
fromapollo-datasource-rest
insrc/data-sources/track-api.js
:const { RESTDataSource } = require("apollo-datasource-rest"); class TrackApi extends RESTDataSource { constructor() { super(); this.baseURL = "https://odyssey-lift-off-rest-api.herokuapp.com/"; } getTracksForHome() { return this.get("tracks"); // i.e. GET https://odyssey-lift-off-rest-api.herokuapp.com/tracks } getAuthor(authorId) { return this.get(`author/${authorId}`); // i.e. GET https://odyssey-lift-off-rest-api.herokuapp.com/author/{authorId} } } module.exports = TrackApi;
- As seen above, you can create a wrapper over a REST api by defining a class for
TrackApi
that extendsRESTDataSource
baseURL
in constructor needs to be set to REST Api host- You can define your functions that make calls using REST verbs over
this
property - The first argument accepted here defines the path of the endpoint on the REST api host
- As seen above, you can create a wrapper over a REST api by defining a class for
-
And here is a sample of
src/resolver.js
that would useTrackApi
to pull data:const resolvers = { Query: { // returns collection of tracks with their authors tracksForHome: (_, __, { dataSources }) => { return dataSources.trackApi.getTracksForHome(); }, }, Track: { author: ({ authorId }, _, { dataSources }) => { return dataSources.trackApi.getAuthor(authorId); }, }, }; module.exports = resolvers;
- The unused arguments as seen above are replaced with
_
(s) in relevant order to make them unique - The query does the job of resolving data
- For any nested relationships, to fetch child data, you will need to define the top level field with child field in that entity that uses a resolver function to populate relevant data. i.e.
Track
entity resolvingauthor
information throughdataSources
using theauthorId
found inparent
Track entity data.
- The unused arguments as seen above are replaced with
-
Finally, you can now replace
mocks
returned from server with actual data using resolvers and datasources as follows:const { ApolloServer } = require("apollo-server"); const typeDefs = require("./schema"); const resolvers = require("./resolvers"); const TrackApi = require("./data-sources/track-api"); const server = new ApolloServer({ typeDefs, resolvers, dataSources: () => ({ trackApi: new TrackApi(), }), }); server.listen().then(() => { console.log(` 🚀 Server is running! 🔉 Listening on port 4000 `); });
mocks
was removed in its entirety here and instead was replaced withresolvers
anddataSources
here that query actual data
If you are a beginner with Apollo GraphQL, I would highly recommend https://odyssey.apollographql.com for good learning experience.