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.
- In
AppModule, add GraphQL with the Apollo driver: the schema generated in memory from the code, GraphiQL served, introspection allowed. - Make
Catan object type with the descriptionA cat the shelter looks after.idandageare integers,nameandbreedstrings, andnicknameis a string that may be null. - Make
CatsResolvera resolver forCat, with acatsquery returning every cat and acatquery that takes an integeridand returns that cat ornull.
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":Cathas 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 | nullhas no runtime type. Give it() => String.Cannot return null for non-nullable field Cat.nickname.: Luna has no nickname and the field is notnullable.- The introspection query shows
ageasFloat: it has@Field()without() => Int.
Remember
- One endpoint, one schema; the client chooses the fields.
@ObjectType()and@Field()build the schema;numberisFloatunless you sayInt.- 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 }).