Code-First GraphQL

GraphQLModule serves one /graphql route, and in code-first mode your decorated TypeScript classes are the schema.

SyntaxGraphQLModule.forRoot<ApolloDriverConfig>({ driver: ApolloDriver, autoSchemaFile: 'src/schema.gql' })

A GraphQL API is a single HTTP route. The client POSTs a query naming exactly the fields it wants, and gets back that shape. That is worth doing when one screen needs data from four tables and you would otherwise ship four REST endpoints or over-fetch from one. It is not worth doing for a webhook receiver or a file download.

The module and the driver

GraphQL arrives as a module: a class that groups providers so other modules can import them. GraphQLModule.forRoot() is a dynamic module, a module you configure with arguments. It needs a driver — the adapter that owns the real GraphQL server. ApolloDriver from @nestjs/apollo wraps Apollo Server 5.

npm i @nestjs/graphql @nestjs/apollo @apollo/server @as-integrations/express5 graphql

NestJS 12 pairs with @nestjs/graphql 14 and @nestjs/apollo 14, and that pair requires Apollo Server 5.

Code-first

Schema-first means you write SDL files and point typePaths at them. Code-first means you decorate classes and Nest builds the schema at startup: @ObjectType() registers a class as a GraphQL type, @Field() registers one property as a field on it. autoSchemaFile set to a path writes the generated SDL there on every boot, so you can commit it; autoSchemaFile: true keeps it in memory only.

What trips people up

Nest emits only the types it can reach from a query, mutation or field. A decorated class that nothing references never appears in the schema; add it to buildSchemaOptions.orphanedTypes if you need it. And graphiql has replaced the old playground: in @nestjs/apollo 14, playground is only a deprecated boolean alias for it.

Example

Example · typescript
// src/app.module.ts
import { join } from 'node:path';
import { Module } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql';
import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo';
import { RecipesModule } from './recipes/recipes.module.js';

@Module({
  imports: [
    // The type argument is what gives you completion on the Apollo-specific
    // options below; the driver is what actually runs the server.
    GraphQLModule.forRoot<ApolloDriverConfig>({
      driver: ApolloDriver,
      // Code-first: Nest builds the schema from the decorated classes it finds
      // in the imported modules, then writes the SDL here on every boot.
      autoSchemaFile: join(process.cwd(), 'src/schema.gql'),
      // Without this the field order follows discovery order, so the committed
      // file churns whenever you move a class.
      sortSchema: true,
      // GraphiQL is already on outside production; this makes it explicit.
      graphiql: process.env.NODE_ENV !== 'production',
      // Whatever this returns is the GraphQL context object every resolver
      // can read with @Context(). Called once per HTTP request.
      context: ({ req, res }) => ({ req, res }),
      path: '/graphql', // the default; one route for the whole API
    }),
    RecipesModule,
  ],
})
export class AppModule {}

When to use it

  • Serve one endpoint a mobile screen can shape itself, instead of adding a REST route per screen.
  • Generate src/schema.gql from the model classes so a removed field shows up in the pull request diff.
  • Keep GraphiQL and introspection on in staging and off in production from a single config value.
  • Put a GraphQL layer over services you already have, since a resolver is just another provider.

More examples

@Field() shorthand next to the longhand

The four forms sit side by side so you can read either in someone else's code: @Field() and @Field(() => String, { nullable: false }) produce exactly the same SDL, and the explicit form is mandatory the moment the runtime metadata is ambiguous (ID, Int, arrays, enums). Reach for this file layout whenever you are converting an existing entity or DTO into a GraphQL type.

Example · typescript
// src/recipes/recipe.model.ts
import { Field, ID, Int, ObjectType } from '@nestjs/graphql';

@ObjectType({ description: 'A single recipe' })
export class Recipe {
  // Longhand is required here: TypeScript emits String for both `string`
  // and an ID, so Nest cannot tell them apart without the type function.
  @Field(() => ID)
  id: string;

  // Shorthand. @Field() reads the design:type metadata the compiler emits,
  // sees String, and produces `title: String!`.
  // Identical to: @Field(() => String, { nullable: false })
  @Field()
  title: string;

  // nullable: true is what removes the `!`. There is no shorthand for it.
  @Field(() => String, { nullable: true })
  description?: string;

  // `number` erases to Number, which maps to Float by default. Say Int.
  @Field(() => Int)
  servings: number;

  // Arrays erase to Array, so the item type must be written out.
  @Field(() => [String])
  tags: string[];

  // Date is one of the four types @Field() can infer; it maps to the
  // built-in DateTime scalar (buildSchemaOptions.dateScalarMode: 'isoDate').
  @Field()
  createdAt: Date;
}

What autoSchemaFile writes out

This is the file the Recipe class above produces, with sortSchema: true (hence the alphabetical fields). It is worth reading once: the `!` marks come from nullable, the DateTime scalar was added for you, and `[Recipe!]!` means a non-null list of non-null items. Commit this file and a reviewer can see a field being removed, which is the cheapest breaking-change check you will get.

Example · graphql
# src/schema.gql — generated, do not edit by hand
"""
A date-time string at UTC, such as 2019-12-03T09:54:33Z, compliant with the date-time format.
"""
scalar DateTime

type Query {
  allRecipes: [Recipe!]!
}

"""A single recipe"""
type Recipe {
  createdAt: DateTime!
  description: String
  id: ID!
  servings: Int!
  tags: [String!]!
  title: String!
}

Configuring the module from the environment

Use this form as soon as any GraphQL option depends on configuration — turning GraphiQL and introspection off per environment, or reading a schema path from an env var. The important detail is that the return type omits `driver`, so passing it inside useFactory will not type-check.

Example · typescript
// src/graphql.config.ts
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { GraphQLModule } from '@nestjs/graphql';
import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo';

@Module({
  imports: [
    // forRootAsync is the same module, configured from providers instead of
    // literals. `driver` stays outside useFactory: Nest needs it before the
    // factory runs.
    GraphQLModule.forRootAsync<ApolloDriverConfig>({
      driver: ApolloDriver,
      imports: [ConfigModule],
      inject: [ConfigService],
      useFactory: (config: ConfigService) => ({
        autoSchemaFile: true, // in memory; nothing written to disk
        graphiql: config.get<string>('NODE_ENV') !== 'production',
        introspection: config.get<string>('NODE_ENV') !== 'production',
        includeStacktraceInErrorResponses: false,
      }),
    }),
  ],
})
export class GraphqlConfigModule {}

Discussion

  • Be the first to comment on this lesson.