Overview
We've got our resolvers and data source ready, but they don't know yet how to work together.
Apollo Server is where all the elements we've built previously (the schema, the resolvers, and the data sources) come together in perfect coordination.
In this lesson, we will:
- Bring together all the pieces of our GraphQL server: the schema, resolvers, and data source
- Update our codegen config file to include our server's data source
Updating our server
In src/index.ts
, we can now tell our server all about our resolvers and data source.
Let's import our resolvers
and ListingAPI
at the top.
import { resolvers } from "./resolvers";import { ListingAPI } from "./datasources/listing-api";
We'll update the ApolloServer
options to accept resolvers
in addition to typeDefs
.
const server = new ApolloServer({typeDefs,resolvers,});
To connect our server with our ListingAPI
, we'll jump down to the startStandaloneServer
function. This function takes a second argument, which is an object for configuring your server's options.
const { url } = await startStandaloneServer(server, {// TODO: configure server options});
This is where we'll define a context
function that returns an object that all our resolvers will share: contextValue
(that third positional argument we talked about earlier!).
Let's set a context
property as an async function, which returns an object.
const { url } = await startStandaloneServer(server, {context: async () => {// this object becomes our resolver's contextValue, the third positional argumentreturn {// TODO};},});
Remember, we want to access the dataSources.listingAPI
(and its methods) from the contextValue
parameter of our resolvers. So let's return an object that allows us to do just that!
We'll set a dataSources
property inside the object, which is set to another object. This object will have a listingAPI
key (lowercase), which returns an instance of the ListingAPI
data source class we imported earlier.
const { url } = await startStandaloneServer(server, {context: async () => {return {dataSources: {listingAPI: new ListingAPI(),},};},});
Our resolver functions expect to find dataSources.listingAPI
on their contextValue
, which is why we've defined a property called dataSources
here, with another property called listingAPI
. Naming the property dataSources
isn't a requirement—we chose dataSources
as a matter of convention. You can give this property whatever name you'd like, but be sure that you update your resolver functions to access the same property.
One last thing! To take advantage of the RESTDataSource
's caching capabilities, we need to pass in the server's cache to our instance of ListingAPI
.
Just before we return the contextValue
object, let's destructure the cache
property from the server
. Then, we'll pass in an object to the ListingAPI
class, containing that cache
property.
const { url } = await startStandaloneServer(server, {context: async () => {const { cache } = server;return {dataSources: {listingAPI: new ListingAPI({ cache }),},};},});
To learn more about the options that ApolloServer
can receive, check out the documentation.
Updating codegen
Technically, all the pieces of our server are now in place! We've instantiated our data source on our server, which takes care of passing it to our resolver functions as contextValue
, their third positional parameter.
There's just one little problem: if we return to resolvers.ts
and hover over our featuredListings
resolver's dataSources
parameter, we'll see that it still has an implicit type of any
!
(parameter) dataSources: any
Let's review that Resolvers
type in types.ts
again. Here, we can see that it actually accepts a type parameter called ContextType
. Whatever we define as this ContextType
is received by our resolver functions.
export type Resolvers<ContextType = any> = {Listing?: ListingResolvers<ContextType>;Query?: QueryResolvers<ContextType>;};
This parameter represents the type of data we set on the server's context
property. It lets us describe more accurately what kind of data our resolver functions have access to on their third positional parameter, contextValue
; we can determine the methods that are available for them to call, and what kinds of data those methods will return.
We know what kind of data is set on our server's context
(an instance of ListingAPI
), but we haven't told TypeScript about it—let's give it a bit more information about our class so it can help us as we code.
In the src
directory, we'll create a new file called context.ts
. This is where we'll define the type that describes the context we pass to our server.
📂 src┣ 📂 datasources┣ 📄 context.ts┣ 📄 graphql.d.ts┣ 📄 helpers.ts┣ 📄 index.ts┣ 📄 resolvers.ts┣ 📄 schema.graphql┗ 📄 types.ts
Inside, we'll define and export a type called DataSourceContext
. Inside of DataSourceContext
we'll define a property called dataSources
, which is an object.
export type DataSourceContext = {dataSources: {};};
We want to give the dataSources
object the same property that we set on our server, so we can add a listingAPI
key, along with the ListingAPI
class. Be sure to import this class as well!
import { ListingAPI } from "./datasources/listing-api";export type DataSourceContext = {dataSources: {listingAPI: ListingAPI;};};
Note: We don't need to instantiate the ListingAPI
class here—this is enough to give our type definition the information it needs about the methods and properties available on the data source class.
Updating codegen.ts
With our DataSourceContext
type defined, we can update our codegen.ts
file to take it into consideration.
Just below the plugins
key, we can add a new config
property. This is an object that specifies a contextType
.
const config: CodegenConfig = {schema: "./src/schema.graphql",generates: {"./src/types.ts": {plugins: ["typescript", "typescript-resolvers"],config: {contextType:},},},};
As the value of contextType
, we'll pass the filepath to our context.ts
file, relative to the ./src/types.ts
file. Our context.ts
file is located in the same src
folder, so our path is "./context"
. Finally, to point to the type we defined in the file, we can tack on #DataSourceContext
to the end of the file path.
config: {contextType: "./context#DataSourceContext",},
Note: Make sure that there is NOT a forward slash (/
) separating ./context
and #DataSourceContext
in the file path you specify.
Finally, let's run our codegen command again!
npm run generate
Now when we reopen the types.ts
file and scroll down to our Resolvers
type, we'll see that the ContextType
defaults to DataSourceContext
!
export type Resolvers<ContextType = DataSourceContext> = {Listing?: ListingResolvers<ContextType>;Query?: QueryResolvers<ContextType>;};
And back in resolvers.ts
, we can hover over dataSources
to see that our type has been inferred correctly. It's an object with a listingAPI
property!
(parameter) dataSources: {listingAPI: ListingAPI;}
(This means if we try to use a method that our resolvers don't have access to, we'll get an error telling us!)
Querying data
With our server still running, let's jump back to the Explorer at http://localhost:4000.
Open up the Settings panel and make sure that we've switched Mock Responses into the OFF position.
Let's try the same query again.
query FeaturedListings {featuredListings {idtitlenumOfBedscostPerNightclosedForBookings}}
And now... we've got real data! 🎉
Practice
DataSourceContext
?Define a data source for resolvers to access. Pass a second argument (a config object) to the startStandaloneServer
function. This object should contain an async context
function that returns an object containing dataSources
. Define a planetService
property and instantiate a new PlanetService
. Then, access the server's cache
and pass it into the PlanetService
constructor for a performance boost!
Key takeaways
- We can use the Apollo Server's
context
property to define the data sources our resolvers need to access. - The GraphQL Code Generator allows us to specify the type of context our resolvers get access to, so we can benefit from type safety throughout our project.
Up next
Amazing! We've translated the REST API response to the types that we defined in the schema, and we have a seamless querying experience. But right now, our list of listings is pretty limited. Based on our mockups, we need to start exploring some additional functionality—such as querying for additional listing properties (hello, amenities!) or asking for a particular listing. Let's uplevel our GraphQL API in the next lesson.
Share your questions and comments about this lesson
Your feedback helps us improve! If you're stuck or confused, let us know and we'll help you out. All comments are public and must follow the Apollo Code of Conduct. Note that comments that have been resolved or addressed may be removed.
You'll need a GitHub account to post below. Don't have one? Post in our Odyssey forum instead.