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.
Table of Contents
- 1. Schema-First vs. Code-First: two philosophies
- 2. Schema-First: the SDL as a contract between teams
- 3. Code-First: generating the schema from code
- 4. Tooling ecosystem compared
- 5. Team structure: who benefits from which approach?
- 6. Schema evolution and versioning in both models
- 7. Type safety: codegen vs. native types
- 8. Migration paths between the approaches
- 9. Schema-First and Code-First compared directly
- 10. Summary
- 11. FAQ
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.