Odyssey

Intro to GraphQL with TypeScript & Apollo Server

Course overview and setupGraphQL basicsSchema definition language (SDL)Building the schemaApollo ServerApollo Sandbox ExplorerThe listings REST APIResolversCodegenQuerying real dataQuery argumentsAdding the Amenity typeResolver chainsMutations
9. Codegen
4m

Overview

We're missing a bit of type safety in our GraphQL server. We need a way to translate our GraphQL schema types into TypeScript types we can pass to our ListingAPI class and resolver functions. Writing these types out by hand takes a lot of work, and it's easy for them to get out of sync with our GraphQL schema. Fortunately, the GraphQL Code Generator is here to help!

In this lesson, we will:

  • Generate the server-side types for our resolver functions and data source

GraphQL Codegen

The GraphQL Code Generator reads in a GraphQL schema and generates TypeScript types we can use throughout our server. It keeps our TypeScript types from getting outdated as we make changes to the schema—allowing us to focus on developing our schema, rather than constantly updating type definitions! Our schema should be our GraphQL server's source of truth, and the GraphQL Code Generator lets us treat it as such.

Installing dependencies

Let's start by navigating to a terminal in the root of our project, and installing three development dependencies: @graphql-codegen/cli, @graphql-codegen/typescript and @graphql-codegen/typescript-resolvers.

npm install -D @graphql-codegen/cli @graphql-codegen/typescript @graphql-codegen/typescript-resolvers

Next, let's create a codegen command that we can run from our package.json file. Add a new entry called generate under the scripts object, and set it to graphql-codegen.

package.json
"scripts": {
"compile": "tsc",
"dev": "ts-node-dev --respawn ./src/index.ts",
"start": "npm run compile && nodemon ./dist/index.js",
"test": "jest",
"generate": "graphql-codegen"
},

To run this command successfully, we need a file that will contain the instructions the GraphQL Code Generator can follow.

The codegen file

Let's create a file called codegen.ts in the root of our project.

📦 intro-typescript
┣ 📂 src
┣ 📄 codegen.ts
┣ 📄 package.json
┣ 📄 README.md
â”— đź“„ tsconfig.json

This file will import CodegenConfig from @graphql-codegen/cli, which we'll use to set all of our codegen details.

codegen.ts
import type { CodegenConfig } from "@graphql-codegen/cli";
const config: CodegenConfig = {};
export default config;

The first property we can set in the config object is where our schema lives. We'll pass in the file path relative to this current directory.

codegen.ts
const config: CodegenConfig = {
schema: "./src/schema.graphql",
};

Next, we define where the generated types should be outputted. Under a key called generates we'll add an object containing our desired path of src/types.ts. This will create a new file called types.ts in our server's src folder.

codegen.ts
const config: CodegenConfig = {
schema: "./src/schema.graphql",
generates: {
"./src/types.ts": {},
},
};

Lastly, let's tell the Code Generator which plugins to use, under a plugins key. This is an array that contains typescript and typescript-resolvers, to point to our two plugins.

codegen.ts
generates: {
"./src/types.ts": {
plugins: ["typescript", "typescript-resolvers"],
},
},

So, what exactly are these plugins responsible for in the code generation process?

Well, @graphql-codegen/typescript is the base plugin needed to generate TypeScript types from our schema.

And @graphql-codegen/typescript-resolvers does something similar: it will review our schema, consider the types and fields we've defined, and output the types we need to accurately describe what data our resolver functions use and return.

import type { CodegenConfig } from "@graphql-codegen/cli";
const config: CodegenConfig = {
schema: "./src/schema.graphql",
generates: {
"./src/types.ts": {
plugins: ["typescript", "typescript-resolvers"],
},
},
};
export default config;

Generating types

We have everything we need to run the codegen command. Open up a terminal in the root directory and run the following command.

npm run generate

After a few moments, we should see that a new file has been added to our server's src directory: types.ts! Let's take a look.

Error: Unable to find Codegen config file!

You'll see this error if the Codegen CLI is unable to locate a codegen.ts file in the root of your project.

How to fix it: Double check that you placed the codegen.ts at the root of the project, and not in the src directory!

Still having trouble? Visit the Odyssey forums to get help.

At the bottom of the file, we'll find type Resolvers. This type is generated from looking at the objects in our schema; we have just two so far, Query and Listing.

src/types.ts
export type Resolvers<ContextType = any> = {
Listing?: ListingResolvers<ContextType>;
Query?: QueryResolvers<ContextType>;
};

The code starts to look a bit complex, but under the hood TypeScript has done all of the validation against our schema to provide us with types we can use to represent the following:

  1. The type of data that enters each of our resolvers
  2. The properties on that data that the resolvers have access to
  3. The type of data that each resolver function returns

As a result, we can use this Resolvers type to bring type safety—and some helpful code validation—to our resolvers object.

Back in resolvers.ts, we'll import this type and apply it to our resolvers object.

resolvers.ts
import { Resolvers } from "./types";
export const resolvers: Resolvers = {
Query: {
featuredListings: (_, __, { dataSources }) => {
return dataSources.listingAPI.getFeaturedListings();
},
},
};

This takes care of our red squiggly errors in our resolvers!

Adding type annotations in listing-api.ts

Our codegen output took care of generating precise TypeScript types for the object types in our schema—giving us a Listing type that we can use in our server code.

types.ts
/** A particular intergalactic location available for booking */
export type Listing = {
__typename?: "Listing";
/** Indicates whether listing is closed for bookings (on hiatus) */
closedForBookings?: Maybe<Scalars["Boolean"]["output"]>;
/** The cost per night */
costPerNight?: Maybe<Scalars["Float"]["output"]>;
id: Scalars["ID"]["output"];
/** The number of beds available */
numOfBeds?: Maybe<Scalars["Int"]["output"]>;
/** The listing's title */
title: Scalars["String"]["output"];
};

Let's return to our data source file: datasources/listing-api.ts. At the top of the file, we'll import the Listing type from types.ts.

datasources/listing-api.ts
import { Listing } from "../types";

Using this type, we can annotate the getFeaturedListings method with a return type of Promise<Listing[]>. We'll also update the type variable we pass into the this.get call to be Listing[] as well.

listing-api.ts
getFeaturedListings(): Promise<Listing[]> {
return this.get<Listing[]>("featured-listings");
}

Great! We're all set with type safety in our listing-api.ts file as well.

import { RESTDataSource } from "@apollo/datasource-rest";
import { Listing } from "../types";
export class ListingAPI extends RESTDataSource {
baseURL = "https://rt-airlock-services-listing.herokuapp.com/";
getFeaturedListings(): Promise<Listing[]> {
return this.get<Listing[]>("featured-listings");
}
}

Now, you might be asking yourself, how do our resolver functions actually get access to the data source we've just created? In other words, how do we populate that third positional argument they receive, contextValue, with the ListingAPI class we want to use?

Fantastic question! The answer is that we actually haven't hooked up that piece of our server just yet. We'll tackle this in the next lesson!

Practice

Which of the following is NOT included in the codegen output?
Code Challenge!

Complete the following codegen.ts file. It should read in the schema at ./src/schema.graphql and generate a file called types.ts file in the src folder. Include the plugins 'typescript' and 'typescript-resolvers'.

Key takeaways

  • Using the GraphQL Code Generator, we can generate types from our schema fields and use them directly in our code. Hello, type safety!
  • To run codegen on our GraphQL schema, we define a codegen.ts file in our project.
  • The codegen.ts file should specify the following properties at minimum:
    • schema, which specifies the path to the GraphQL schema
    • generates, which specifies the path where the generated types should be outputted
    • plugins, which lists the plugins that should be incorporated into the codegen process
  • The typescript codegen plugin is the base plugin needed to generate TypeScript types from a GraphQL schema.
  • The typescript-resolvers codegen plugin will augment the codegen process to include a Resolvers type we can use to annotate our resolvers with a single, comprehensive type.

Up next

Schema, data sources, and resolvers? We've got them all. But they're not yet working together. If we ran our server, it would have no clue about ListingAPI—and it also doesn't know about our resolvers! Let's make sure all the pieces know about each other, and send our first queries for real data in the next lesson.

Previous
Next

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.

              GraphQL server

              A server that contains a GraphQL schema and can resolve client-requested operations that are executed against that schema.

              GraphQL schema

              A GraphQL schema defines the structure and types of data that can be queried or mutated, serving as a contract between the server and clients.

              resolver

              A function that populates data for a particular field in a GraphQL schema. For example:

              const resolvers = {
              Query: {
              author(root, args, context, info) {
              return find(authors, { id: args.id });
              },
              },
              };
              GraphQL schema

              A GraphQL schema defines the structure and types of data that can be queried or mutated, serving as a contract between the server and clients.

              resolver

              A function that populates data for a particular field in a GraphQL schema. For example:

              const resolvers = {
              Query: {
              author(root, args, context, info) {
              return find(authors, { id: args.id });
              },
              },
              };
              GraphQL schema

              A GraphQL schema defines the structure and types of data that can be queried or mutated, serving as a contract between the server and clients.

              GraphQL server

              A server that contains a GraphQL schema and can resolve client-requested operations that are executed against that schema.

              GraphQL

              An open-source query language and specification for APIs that enables clients to request specific data, promoting efficiency and flexibility in data retrieval.

              fields

              A unit of data that belongs to a type in a schema. Every GraphQL query requests one or more fields.

              type Author {
              # id, firstName, and lastName are all fields of the Author type
              id: Int!
              firstName: String
              lastName: String
              }
              resolver

              A function that populates data for a particular field in a GraphQL schema. For example:

              const resolvers = {
              Query: {
              author(root, args, context, info) {
              return find(authors, { id: args.id });
              },
              },
              };
              resolver

              A function that populates data for a particular field in a GraphQL schema. For example:

              const resolvers = {
              Query: {
              author(root, args, context, info) {
              return find(authors, { id: args.id });
              },
              },
              };
              resolvers

              A function that populates data for a particular field in a GraphQL schema. For example:

              const resolvers = {
              Query: {
              author(root, args, context, info) {
              return find(authors, { id: args.id });
              },
              },
              };
              resolver

              A function that populates data for a particular field in a GraphQL schema. For example:

              const resolvers = {
              Query: {
              author(root, args, context, info) {
              return find(authors, { id: args.id });
              },
              },
              };
              resolvers

              A function that populates data for a particular field in a GraphQL schema. For example:

              const resolvers = {
              Query: {
              author(root, args, context, info) {
              return find(authors, { id: args.id });
              },
              },
              };
              object types

              A type in a GraphQL schema that has one or more fields. User is an object type in the following example:

              type User {
              name: String!
              }
              variable

              A placeholder for dynamic values in an operation allowing parameterization and reusability in requests. Variables can be used to fill arguments or passed to directives.

              query GetUser($userId: ID!) {
              user(id: $userId) {
              firstName
              }
              }

              In the query above, userId is a variable. The variable and its type are declared in the operation signature, signified by a $. The type of variable is a non-nullable ID. A variable's type must match the type of any argument it's used for.

              resolver

              A function that populates data for a particular field in a GraphQL schema. For example:

              const resolvers = {
              Query: {
              author(root, args, context, info) {
              return find(authors, { id: args.id });
              },
              },
              };
              argument

              A key-value pair associated with a particular schema field that lets operations pass data to that field's resolver.

              Argument values can be hardcoded as literal values (shown below for clarity) or provided via GraphQL variables (recommended).

              query GetHuman {
              human(id: "200") {
              name
              height(unit: "meters")
              }
              }
              GraphQL

              An open-source query language and specification for APIs that enables clients to request specific data, promoting efficiency and flexibility in data retrieval.

              fields

              A unit of data that belongs to a type in a schema. Every GraphQL query requests one or more fields.

              type Author {
              # id, firstName, and lastName are all fields of the Author type
              id: Int!
              firstName: String
              lastName: String
              }
              GraphQL schema

              A GraphQL schema defines the structure and types of data that can be queried or mutated, serving as a contract between the server and clients.

              GraphQL schema

              A GraphQL schema defines the structure and types of data that can be queried or mutated, serving as a contract between the server and clients.

              GraphQL schema

              A GraphQL schema defines the structure and types of data that can be queried or mutated, serving as a contract between the server and clients.

              resolvers

              A function that populates data for a particular field in a GraphQL schema. For example:

              const resolvers = {
              Query: {
              author(root, args, context, info) {
              return find(authors, { id: args.id });
              },
              },
              };
              resolvers

              A function that populates data for a particular field in a GraphQL schema. For example:

              const resolvers = {
              Query: {
              author(root, args, context, info) {
              return find(authors, { id: args.id });
              },
              },
              };
              resolvers

              A function that populates data for a particular field in a GraphQL schema. For example:

              const resolvers = {
              Query: {
              author(root, args, context, info) {
              return find(authors, { id: args.id });
              },
              },
              };

              NEW COURSE ALERT

              Introducing Apollo Connectors

              Connectors are the new and easy way to get started with GraphQL, using existing REST APIs.

              Say goodbye to GraphQL servers and resolvers—now, everything happens in the schema!

              Take the course