GraphQL Schema-First vs. Code-First: Pros and Cons Compared
AI generated
{ }
type
GraphQL · Schema Design · Tooling · Architecture
Schema-First vs. Code-First
which GraphQL approach fits which team

With Schema-First, the SDL file comes first and becomes the contract between frontend and backend. With Code-First, a library generates the schema from classes and decorators. Both paths produce a working GraphQL API, yet they differ fundamentally in tooling, team structure, and long-term maintainability.

16 min read SDL · graphql-php · TypeGraphQL · Codegen GraphQL · Architecture · Team Workflow

1. Schema-First vs. Code-First: two philosophies

Anyone setting up a new GraphQL API faces an early architectural decision that becomes hard to reverse later: is the schema written first as an SDL file (Schema-First), or does it emerge automatically from classes, decorators, and annotations in the application code (Code-First)? Both approaches end up producing the same introspectable GraphQL API, but the path there differs fundamentally, and this decision shapes how a team thinks about its API for years.

The core difference lies in the question: what is the single source of truth? With Schema-First, it is a readable .graphqls file that exists before implementation and can be negotiated jointly by frontend and backend developers. With Code-First, it is the programming language itself: classes, types, and decorators define the schema, which is only generated at build time or runtime. Both philosophies earn their place in large production systems, and the following sections show exactly where the pros and cons lie.

2. Schema-First: the SDL as a contract between teams

In the Schema-First approach, a developer writes the Schema Definition Language before a single line of resolver code exists. This SDL file becomes a binding contract: frontend teams can develop against the schema as soon as it is in place, regardless of whether the resolver implementation is finished. Mocking tools such as graphql-tools or Apollo Server's addMocksToSchema generate working test data directly from the pure SDL file, without any backend code needing to exist. That decouples frontend and backend development in time, which is especially valuable during parallel sprints.

Magento's own GraphQL implementation consistently follows the Schema-First principle: every module ships a schema.graphqls file that gets wired to resolver classes via di.xml. This separation forces developers to treat the schema as a standalone artifact that can be reviewed on its own in code review. One downside shows up as schemas grow: the SDL file and the resolver class drift apart if nobody consistently checks that every SDL field actually has a resolver, because the coupling is enforced only by convention and configuration, not by the compiler.


# schema.graphqls — Schema-First: SDL is written before any resolver exists
type Product {
  id: ID!
  sku: String!
  name: String!
  price: Money!
  # Nullable on purpose — not every product has a manufacturer set
  manufacturer: Manufacturer
  reviews(first: Int = 10, after: String): ReviewConnection!
}

type Money {
  amount: Float!
  currency: CurrencyEnum!
}

type Query {
  product(sku: String!): Product
  products(filter: ProductFilterInput, pageSize: Int = 20): ProductConnection!
}

input ProductFilterInput {
  category: String
  minPrice: Float
  maxPrice: Float
}

3. Code-First: generating the schema from code

The Code-First approach reverses the order: developers write classes, interfaces, or decorators in their application language, and a library generates the GraphQL schema from that at build time or runtime. In PHP, graphql-php handles this in Code-First mode through ObjectType definitions; in TypeScript, TypeGraphQL with decorators or Nexus with a declarative builder API do the job. The big advantage: there is no second source of truth that needs to be kept in sync. Change a class, and the schema changes automatically too, because both are identical.

This property makes Code-First especially attractive for teams already using strongly typed backend languages such as TypeScript or PHP with strict typing. A developer who adds a new property to a TypeScript class sees immediate compiler errors if a resolver uses that property incorrectly, with no manual schema update required. The downside: the schema no longer exists as a standalone, readable artifact that frontend teams can negotiate up front. Anyone who wants to see the final SDL has to generate the code or reach for an introspection tool, which makes early alignment between API consumer and API provider harder.


// product.type.ts — Code-First with TypeGraphQL: schema is generated from classes
import { ObjectType, Field, ID, Float, Resolver, Query, Arg } from 'type-graphql';

@ObjectType()
class Money {
  @Field(() => Float)
  amount: number;

  @Field()
  currency: string;
}

@ObjectType()
class Product {
  @Field(() => ID)
  id: string;

  @Field()
  sku: string;

  @Field()
  name: string;

  @Field(() => Money)
  price: Money;

  // Nullable field — TypeScript's optional marker maps directly to GraphQL nullability
  @Field(() => String, { nullable: true })
  manufacturer?: string;
}

@Resolver(Product)
class ProductResolver {
  @Query(() => Product, { nullable: true })
  async product(@Arg('sku') sku: string): Promise<Product | null> {
    return productRepository.findBySku(sku);
  }
}

// ProductType.php — Code-First with webonyx/graphql-php: schema built from PHP classes
use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\Type;

final class ProductType extends ObjectType
{
    public function __construct()
    {
        parent::__construct([
            'name' => 'Product',
            'fields' => fn(): array => [
                'id' => Type::nonNull(Type::id()),
                'sku' => Type::nonNull(Type::string()),
                'name' => Type::nonNull(Type::string()),
                'price' => Type::nonNull(MoneyType::instance()),
                // Nullable — no separate SDL file to keep in sync
                'manufacturer' => Type::string(),
            ],
            'resolveField' => function (Product $product, array $args, $context, $info) {
                return $product->{$info->fieldName} ?? null;
            },
        ]);
    }
}

4. Tooling ecosystem compared

Tooling around Schema-First is historically the more mature ecosystem, because GraphQL itself was introduced with a textual schema language. Tools such as GraphQL Code Generator read the SDL file and produce type-safe resolver signatures, frontend hooks, and even complete TypeScript interfaces from it. Schema linters like graphql-schema-linter check naming conventions and best practices directly against the SDL file, without executing any application code. Schema registries such as Apollo Studio or Hive also work primarily with the SDL as the artifact that gets versioned and diffed.

For Code-First, an equally mature, standalone ecosystem has emerged in recent years: Nexus, Pothos, and TypeGraphQL in the TypeScript world, Lighthouse with attributes in Laravel, and various annotation-based approaches in Java and Kotlin. These tools generate the SDL as a byproduct and usually export it as a file for external consumers, so frontend tooling still works. The difference lies in the workflow: with Code-First, the generated SDL is a build artifact that gets committed or regenerated on every build, whereas with Schema-First it is the primary, hand-maintained source.

5. Team structure: who benefits from which approach?

The choice between Schema-First and Code-First depends heavily on team structure. Larger organizations with separate frontend and backend teams almost always benefit from Schema-First, because the SDL file functions as a standalone negotiation object in design reviews long before backend code exists. A frontend team can develop against a mock server while the backend team implements resolvers in parallel, and both sides synchronize through the file in the Git repository rather than through meeting discussions.

Smaller teams or full-stack developers who write both frontend and backend in the same language, for example TypeScript in a Node.js monorepo, more often benefit from Code-First. Here, the coordination overhead between two separate artifacts disappears, and a single developer can carry a new feature from the database to the resolver without switching back and forth between the SDL file and the implementation code. In mixed PHP projects like Magento, however, Schema-First dominates, because the module architecture already relies on declarative XML and schema files that are versioned independently of the PHP code.

6. Schema evolution and versioning in both models

Schema evolution, meaning the controlled addition, modification, and removal of fields over time, is transparent under Schema-First through diffs of the SDL file. A pull request that removes a field shows exactly that one line as a deletion, letting reviewers instantly recognize whether it constitutes a breaking change. CI pipelines can automatically compare the old and new SDL files and block the merge on incompatible changes before any consumer is affected.

With Code-First, schema evolution is subtler to monitor because the schema only manifests after the build or at runtime. A refactor that renames a TypeScript class can inadvertently change a GraphQL field without the developer perceiving it as an API change. That is why, in Code-First projects, exporting the generated SDL as an artifact and checking it with diff tools too is mandatory, otherwise you lose exactly the visibility that Schema-First provides by default.


#!/usr/bin/env bash
# ci-schema-diff.sh — works for both approaches once SDL is exported as an artifact
set -euo pipefail

# Schema-First: SDL file is already the source of truth
# Code-First: export the generated schema first, e.g. `ts-node export-schema.ts`
CURRENT_SCHEMA="schema.graphqls"
BASELINE_SCHEMA="$(git show origin/main:schema.graphqls)"

diff <(echo "$BASELINE_SCHEMA") "$CURRENT_SCHEMA" > /tmp/schema.diff || true

if grep -qE '^< .*(type|field)' /tmp/schema.diff; then
  echo "[WARN] Possible breaking change detected — review required" >&2
  cat /tmp/schema.diff
  exit 1
fi

echo "[OK] Schema change is additive"

7. Type safety: codegen vs. native types

Type safety in Schema-First is achieved through an additional generation step: GraphQL Code Generator reads the SDL file and produces TypeScript interfaces or PHP stub classes that get used in resolver code. This works well, but requires the codegen step to be re-run on every schema change, otherwise generated types and the actual schema drift apart. Many teams wire this step into a Git hook or a CI check that fails the build if generated types are stale.

With Code-First, type safety is inherent, because the schema arises directly from typed code. A TypeScript compiler error on incorrect field usage appears immediately, without a separate codegen run. This advantage reverses, however, as soon as frontend consumers who don't use the same Code-First stack enter the picture: they still need an exported SDL file and, in doubt, the same codegen step as under Schema-First, which partially cancels out the supposed advantage for cross-team APIs.


# codegen.yml — GraphQL Code Generator config, works against exported SDL
# from EITHER approach (Schema-First file or Code-First export)
schema: "schema.graphqls"
documents: "src/**/*.graphql"
generates:
  src/generated/graphql.ts:
    plugins:
      - typescript
      - typescript-operations
      - typescript-react-apollo
    config:
      withHooks: true
      scalars:
        Money: "{ amount: number; currency: string }"

8. Migration paths between the approaches

A full switch from Code-First to Schema-First usually succeeds step by step: first the generated SDL file is exported and committed into the repository as a standalone artifact. That file then takes over as the source of truth, while the Code-First library gets replaced by classic resolver bindings, module by module rather than in one shot. This migration is typically worthwhile when a project grows and multiple independent frontend teams join, needing a stable, negotiable interface.

The reverse path, from Schema-First to Code-First, is rarer but does happen when a team finds that manual synchronization between the SDL file and resolver code produces too many errors. Here it helps to first trial a single, clearly bounded module on Code-First and gather team experience before tackling a larger switch. In both directions, the same rule applies: a big-bang switch of the entire schema in one day is risky, while a module-by-module migration with an exported SDL as a safety net works reliably.

9. Schema-First and Code-First compared directly

The table below summarizes the central differences between Schema-First and Code-First, based on the tooling, team structure, and maintainability criteria discussed in this article.

Criterion Schema-First Code-First
Source of truth SDL file, hand-maintained Application code, SDL generated
Early frontend/backend split Very good, mock server usable immediately Only with an additional SDL export
Type safety in resolvers Requires a codegen step Native, from typed code
Risk of drift SDL and resolvers can diverge Excluded, both identical
Ideal team size Large, separated teams Small full-stack teams
Typical example Magento GraphQL, Apollo with SDL TypeGraphQL, Nexus, Pothos

No approach is universally superior. The table shows that Schema-First wins on team coordination and explicit API negotiation, while Code-First leads on type safety and avoiding drift. Many production systems combine both principles: a Code-First backend exports its SDL as a versioned artifact and then treats it exactly like a Schema-First file for all external consumers.

Mironsoft

GraphQL architecture, schema design, and API strategy

Finding the right GraphQL approach for your team?

We assess your team structure, existing tooling landscape, and growth plans, and give a concrete recommendation on whether Schema-First or Code-First is the better choice for your next GraphQL project.

Architecture review

Analyze your existing schema strategy and identify improvement potential

Migration

Guide a step-by-step switch between Schema-First and Code-First

Tooling setup

Set up codegen, schema linting, and CI diff checks matched to your approach

10. Summary

Schema-First and Code-First solve the same problem, defining a GraphQL schema, with opposing priorities. Schema-First treats the SDL file as a standalone contract that can be negotiated early between teams and diffed cleanly, but requires discipline to avoid drift between schema and resolver code. Code-First eliminates that drift risk through native type safety, but requires an extra export step as soon as external consumers need a readable SDL.

The right decision depends less on technical superiority than on team structure: separate frontend and backend teams working in different languages usually do better with Schema-First, while full-stack teams working in a single typed language benefit from Code-First. Magento and many enterprise PHP systems consistently rely on Schema-First for historical and architectural reasons, while modern TypeScript monorepos increasingly favor Code-First.

Schema-First vs. Code-First — Key Takeaways

Schema-First

SDL file as a negotiable contract, ideal for separated teams, requires codegen for type safety.

Code-First

Schema generated from typed code, no drift risk, but harder for external teams to negotiate.

Decision criterion

Team structure and language landscape decide, not the technical superiority of one approach.

Hybrid option

Code-First with exported SDL combines native type safety with a negotiable contract.

11. FAQ: Schema-First vs. Code-First

1Core difference?
Schema-First: SDL file first, it is the source of truth. Code-First: schema generated from classes and decorators, the code is the source of truth.
2Better for large teams?
Schema-First, because the SDL file works as a standalone, early-negotiable contract between separated frontend and backend teams.
3Why Schema-First at Magento?
Module architecture relies on declarative files. Every module ships a schema.graphqls, wired to resolvers via di.xml.
4Drift possible with Code-First?
No between schema and code, since both are identical. Only between the exported SDL artifact and the code if the export is stale.
5SDL still needed with Code-First?
Yes, as soon as external teams or tools like codegen, registries, or linters need to consume the API, the generated SDL must be exported.
6Migrating Code-First to Schema-First?
Export the SDL, commit it, then replace the library with classic resolver bindings module by module. No big-bang switch.
7Tooling for Code-First in TypeScript?
TypeGraphQL with decorators, Nexus with a builder API, or Pothos as a modern, plugin-based approach.
8Is Schema-First slower?
Not fundamentally. Extra codegen step needed, but enables parallel frontend development against a mock server.
9Checking breaking changes with Code-First?
Export SDL after every build and diff it against the previous version in CI, exactly like Schema-First.
10Can both approaches be combined?
Yes, Code-First with an automatically exported, versioned SDL, treated like a Schema-First artifact for external consumers.