Code-First vs Schema-First
Before you write a single resolver, NestJS asks you to make one foundational choice: where does your GraphQL schema come from? In the code-first approach you write TypeScript classes decorated with @ObjectType(), @Field(), and friends, and Nest generates the schema for you. In the schema-first approach you author the schema by hand as SDL (Schema Definition Language) .graphql files, and Nest derives TypeScript types from them. Both are first-class, both ship in @nestjs/graphql, and the decision shapes how your team reads, reviews, and evolves the API.
How code-first works
With code-first, the SDL never exists in your source tree — it is an output. You annotate plain TypeScript classes, and at bootstrap Nest reflects over those decorators to build the schema in memory (optionally writing it to disk). Your types and your schema can never drift apart because one generates the other.
// recipe.model.ts
import { Field, ID, ObjectType, Int } from '@nestjs/graphql';
@ObjectType()
export class Recipe {
@Field(() => ID)
id: string;
@Field()
title: string;
@Field({ nullable: true })
description?: string;
@Field(() => Int)
ingredientCount: number;
}
You enable it by pointing autoSchemaFile at a path (or passing true to keep the schema purely in memory):
// app.module.ts
import { Module } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql';
import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo';
import { join } from 'path';
import { RecipesModule } from './recipes/recipes.module';
@Module({
imports: [
GraphQLModule.forRoot<ApolloDriverConfig>({
driver: ApolloDriver,
autoSchemaFile: join(process.cwd(), 'src/schema.gql'),
sortSchema: true,
}),
RecipesModule,
],
})
export class AppModule {}
On startup Nest emits the generated SDL so you can commit and diff it:
Output:
# src/schema.gql — auto-generated, do not edit
type Recipe {
id: ID!
title: String!
description: String
ingredientCount: Int!
}
Tip: Because TypeScript erases types at runtime, you must pass an explicit type thunk for non-
Stringfields —@Field(() => Int),@Field(() => ID),@Field(() => [Recipe]). Relying on inference for numbers or arrays silently produces the wrong schema.
How schema-first works
With schema-first, SDL is the source of truth. You write .graphql files, register them through typePaths (a glob), and Nest stitches them into one schema. Your resolvers must then satisfy that contract. To get TypeScript types, you let Nest generate an interface file from the SDL.
# src/recipes/recipe.graphql
type Recipe {
id: ID!
title: String!
description: String
ingredientCount: Int!
}
type Query {
recipe(id: ID!): Recipe
}
// app.module.ts
import { GraphQLModule } from '@nestjs/graphql';
import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo';
import { join } from 'path';
@Module({
imports: [
GraphQLModule.forRoot<ApolloDriverConfig>({
driver: ApolloDriver,
typePaths: ['./**/*.graphql'],
definitions: {
path: join(process.cwd(), 'src/graphql.ts'),
outputAs: 'class',
},
}),
],
})
export class AppModule {}
The definitions block runs the type generator, producing src/graphql.ts with classes or interfaces that mirror the SDL:
Output:
// src/graphql.ts — auto-generated
export class Recipe {
id: string;
title: string;
description?: Nullable<string>;
ingredientCount: number;
}
Resolvers reference those generated types, but the schema shape is fixed by the .graphql files, not by your classes.
Trade-offs at a glance
| Concern | Code-first | Schema-first |
|---|---|---|
| Source of truth | TypeScript decorators | .graphql SDL files |
| Schema location | Generated (autoSchemaFile) | Hand-written (typePaths) |
| Types stay in sync | Automatic — schema derives from code | Via definitions generation step |
| Best for | TS-centric teams, refactor-heavy domains | Schema-first design, polyglot/contract teams |
| Readability of API | Spread across decorated classes | One canonical SDL document |
| Federation support | Full (@nestjs/apollo federation driver) | Full |
Code-first removes a whole class of drift bugs and keeps everything in one language, which is why most new Nest projects choose it. Schema-first wins when the schema is a contract negotiated across teams or written before any implementation exists — the SDL becomes the artifact everyone reviews.
Configuration reference
| Option | Approach | Purpose |
|---|---|---|
autoSchemaFile | Code-first | Path to write the generated SDL, or true to keep in memory |
sortSchema | Code-first | Alphabetically sort the generated SDL for stable diffs |
typePaths | Schema-first | Glob(s) locating your .graphql SDL files |
definitions | Schema-first | Generates TS types (outputAs: 'class' | 'interface') |
Warning: Do not mix both styles in one schema. Choose code-first or schema-first per
GraphQLModuleinstance; usingautoSchemaFiletogether withtypePathsis unsupported and leads to confusing build behavior.
Best practices
- Default to code-first for greenfield NestJS apps — fewer moving parts and zero schema/type drift.
- Always pass an explicit type thunk (
() => Int,() => [Recipe]) in code-first; never rely on inference for numbers, booleans, or arrays. - Enable
sortSchema: trueand commit the generatedschema.gqlso schema changes show up clearly in code review. - Choose schema-first when an external contract, design-first workflow, or non-TypeScript consumers make the SDL the primary artifact.
- In schema-first, run the
definitionsgenerator as part of your build (or via the standalone generator) so resolvers always type-check against current SDL. - Keep
.graphqlfiles colocated with their feature module sotypePathsglobs stay predictable and ownership is clear.