Skip to content
NestJS ns graphql 4 min read

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-String fields — @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

ConcernCode-firstSchema-first
Source of truthTypeScript decorators.graphql SDL files
Schema locationGenerated (autoSchemaFile)Hand-written (typePaths)
Types stay in syncAutomatic — schema derives from codeVia definitions generation step
Best forTS-centric teams, refactor-heavy domainsSchema-first design, polyglot/contract teams
Readability of APISpread across decorated classesOne canonical SDL document
Federation supportFull (@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

OptionApproachPurpose
autoSchemaFileCode-firstPath to write the generated SDL, or true to keep in memory
sortSchemaCode-firstAlphabetically sort the generated SDL for stable diffs
typePathsSchema-firstGlob(s) locating your .graphql SDL files
definitionsSchema-firstGenerates TS types (outputAs: 'class' | 'interface')

Warning: Do not mix both styles in one schema. Choose code-first or schema-first per GraphQLModule instance; using autoSchemaFile together with typePaths is 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: true and commit the generated schema.gql so 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 definitions generator as part of your build (or via the standalone generator) so resolvers always type-check against current SDL.
  • Keep .graphql files colocated with their feature module so typePaths globs stay predictable and ownership is clear.
Last updated June 14, 2026
Was this helpful?