InteractiveFrameworks

Your first query

Serve the cats as a GraphQL schema: the Apollo driver generating the schema from decorated classes, an object type whose fields say exactly what they are, and a resolver with two queries.

What you'll learn

  • Explain what GraphQL changes from REST: one endpoint, a typed schema, and the client choosing the fields it gets
  • Configure GraphQLModule with the Apollo driver, the schema generated in memory from code, GraphiQL and introspection, and know why production turns the last two off
  • Describe an object type with @ObjectType() and @Field(), and state Int, String and nullability exactly
  • Answer queries with a @Resolver's @Query() methods, taking typed arguments with @Args()

The shelter's mobile app shows a list of cats with their names and photos, and a detail page with everything else. With the REST API it calls GET /cats and receives every field of every cat, most of which the list ignores. Then, for the detail page, it calls again. Each new screen means a new endpoint or a bigger response. GraphQL turns this around: the API publishes a schema, every type and every field it can serve, and the client sends a query naming exactly the fields it wants. The server answers with exactly those, in the shape of the query, at one endpoint.

{ cats { name nickname } }
{ "data": { "cats": [{ "name": "Tom", "nickname": "Tommy" }, { "name": "Luna", "nickname": null }] } }

The module and the schema

@nestjs/graphql builds the GraphQL server into your app, with a driver that does the serving. The docs' default is Apollo Server, from @nestjs/apollo. In code first style you never write the schema: Nest generates it from your decorated classes. autoSchemaFile: true keeps the generated schema in memory:

GraphQLModule.forRoot<ApolloDriverConfig>({
  driver: ApolloDriver,
  autoSchemaFile: true,
  graphiql: true,
  introspection: true,
}),

Introspection is GraphQL's way of asking a server for its own schema. GraphiQL is the in-browser IDE served at the endpoint, which uses introspection for its documentation and autocompletion. Apollo turns both off when NODE_ENV is production, so that a deployed API does not advertise itself. This runtime runs as production, like a deployed app, so both are switched on explicitly here. The endpoint is /graphql: every query is a POST there with a JSON body { "query": "...", "variables": {...} }.

Object types

An object type is a class with @ObjectType(), and each property that belongs in the schema carries @Field():

@ObjectType()
export class Volunteer {
  @Field(() => Int)
  id: number;

  @Field()
  name: string;

  @Field(() => Float, { nullable: true })
  hoursPerWeek: number | null;
}

Nest reads a property's TypeScript type where it can: string becomes String, boolean becomes Boolean. But TypeScript has one number type and GraphQL has two, so a bare number becomes Float. An integer needs () => Int. A union such as number | null has no single runtime type at all, so it needs the type function too.

Every field is non-null unless it says otherwise, which the schema writes as Int!. If a resolver returns null for a non-null field, GraphQL answers Cannot return null for non-nullable field and nulls out the enclosing object. { nullable: true } makes the field optional. A property without @Field() simply is not in the schema.

Resolvers and queries

A resolver is a provider whose methods answer the schema's root fields. @Resolver(() => Cat) names the type it resolves. @Query() declares a root query, with a type function for what it returns:

@Query(() => [Volunteer])
volunteers(): Volunteer[] { ... }

@Query(() => Volunteer, { nullable: true })
volunteer(@Args('id', { type: () => Int }) id: number): Volunteer | null { ... }

The method's name becomes the query's name. @Args('id') declares an argument and hands it to the method, and its type follows the same rule as fields: an integer argument needs { type: () => Int }. A query's type is non-null too, so a lookup that may find nothing must say nullable: true, or a missing cat becomes an error instead of null.

GraphQL checks every query against the schema before running anything. A field that does not exist is refused with a 400 and Cannot query field "colour" on type "Cat". Your resolver never sees the request.

Your task

CatsService holds three cats and CatsModule is registered. Nothing speaks GraphQL yet.

  1. In AppModule, add GraphQL with the Apollo driver: the schema generated in memory from the code, GraphiQL served, introspection allowed.
  2. Make Cat an object type with the description A cat the shelter looks after. id and age are integers, name and breed strings, and nickname is a string that may be null.
  3. Make CatsResolver a resolver for Cat, with a cats query returning every cat and a cat query that takes an integer id and returns that cat or null.

Then open the app in a new tab: GraphiQL at /graphql lists your schema in its documentation panel.

When it fails

  • Cannot determine a GraphQL output type for the "cats": Cat has no @ObjectType(), so Nest does not know it as a GraphQL type.
  • "Query.cats" was defined in resolvers, but not in schema: @Query() has no type function. Code first needs @Query(() => [Cat]).
  • Undefined type error. Make sure you are providing an explicit type for the "nickname": string | null has no runtime type. Give it () => String.
  • Cannot return null for non-nullable field Cat.nickname.: Luna has no nickname and the field is not nullable.
  • The introspection query shows age as Float: it has @Field() without () => Int.

Remember

  • One endpoint, one schema; the client chooses the fields.
  • @ObjectType() and @Field() build the schema; number is Float unless you say Int.
  • Everything is non-null unless it says nullable: true.
  • A @Resolver's @Query() methods answer queries; @Args() brings their arguments.
Stuck? Show a hint

app.module.ts: GraphQLModule.forRoot<ApolloDriverConfig>({ driver: ApolloDriver, autoSchemaFile: true, graphiql: true, introspection: true }). cat.model.ts: @ObjectType() on the class, @Field(() => Int) for integers (a bare number is a Float), @Field(() => String, { nullable: true }) for nickname. cats.resolver.ts: @Resolver(() => Cat), @Query(() => [Cat]), @Query(() => Cat, { nullable: true }) with @Args('id', { type: () => Int }).