APIs GraphQL con tipado seguro mediante generación de código
Guía práctica de tipado seguro de extremo a extremo en GraphQL con generación desde el esquema: resolvers tipados, tipos de consulta y cero discrepancias.

La brecha de tipos en las aplicaciones GraphQL
GraphQL promete un contrato de API tipado, pero la mayoría de las implementaciones tienen una brecha: el esquema define los tipos en SDL, los resolvers usan objetos JavaScript sin tipar y el cliente tipa manualmente las formas de las respuestas. Cualquier discrepancia entre estas tres capas crea errores en tiempo de ejecución que el sistema de tipos debería haber detectado.
La generación de código cierra esta brecha derivando tipos de TypeScript del esquema GraphQL automáticamente, tanto para los resolvers del servidor como para las consultas del cliente.
Diseño schema-first
# schema.graphql — the single source of truth
type User {
id: ID!
email: String!
name: String!
role: UserRole!
posts(limit: Int = 10, offset: Int = 0): [Post!]!
createdAt: DateTime!
}
enum UserRole {
ADMIN
EDITOR
VIEWER
}
type Post {
id: ID!
title: String!
content: String!
author: User!
tags: [String!]!
publishedAt: DateTime
status: PostStatus!
}
enum PostStatus {
DRAFT
PUBLISHED
ARCHIVED
}
type Query {
user(id: ID!): User
users(role: UserRole, limit: Int = 20): [User!]!
post(id: ID!): Post
posts(status: PostStatus, limit: Int = 20): [Post!]!
}
type Mutation {
createUser(input: CreateUserInput!): User!
updatePost(id: ID!, input: UpdatePostInput!): Post!
publishPost(id: ID!): Post!
}
input CreateUserInput {
email: String!
name: String!
role: UserRole!
}
input UpdatePostInput {
title: String
content: String
tags: [String!]
}Generación de tipos para los resolvers
La generación de código transforma el esquema en interfaces de TypeScript que los resolvers deben satisfacer. El compilador exige que cada resolver devuelva la forma correcta: se acabó esperar que tu resolver coincida con el esquema.
// ❌ Untyped resolvers — schema/resolver mismatch is invisible
const resolvers = {
Query: {
user: async (_, { id }) => {
const user = await db.users.findById(id);
return user; // Does this match the User type? Who knows.
},
},
};
// ✅ Generated types enforce schema compliance
// Generated by graphql-codegen from schema.graphql
import type { Resolvers } from "./generated/resolvers-types";
const resolvers: Resolvers = {
Query: {
user: async (_parent, { id }, context) => {
const user = await context.db.users.findById(id);
if (!user) return null;
return {
id: user.id,
email: user.email,
name: user.name,
role: user.role, // Must be UserRole enum
createdAt: user.createdAt,
// TypeScript error if any required field is missing
};
},
users: async (_parent, { role, limit }, context) => {
return context.db.users.findMany({
where: role ? { role } : undefined,
take: limit ?? 20,
});
},
},
User: {
posts: async (parent, { limit, offset }, context) => {
return context.db.posts.findMany({
where: { authorId: parent.id },
take: limit ?? 10,
skip: offset ?? 0,
});
},
},
Mutation: {
createUser: async (_parent, { input }, context) => {
return context.db.users.create({ data: input });
},
publishPost: async (_parent, { id }, context) => {
return context.db.posts.update({
where: { id },
data: { status: "PUBLISHED", publishedAt: new Date() },
});
},
},
};Configuración de codegen
# codegen.yml
schema: "./schema.graphql"
generates:
# Server-side resolver types
./src/generated/resolvers-types.ts:
plugins:
- typescript
- typescript-resolvers
config:
contextType: "../context#GraphQLContext"
mappers:
User: "../models#UserModel"
Post: "../models#PostModel"
scalars:
DateTime: "Date"
# Client-side operation types
./src/generated/operations.ts:
documents: "./src/**/*.graphql"
plugins:
- typescript
- typescript-operations
- typescript-react-apollo
config:
withHooks: true
scalars:
DateTime: "string"Seguridad de tipos en el cliente
El mismo esquema genera hooks tipados para el cliente. Los resultados de las consultas, las entradas de las mutaciones y las variables se verifican contra el esquema: el frontend y el backend tienen garantizado coincidir en las formas.
// src/queries/user.graphql
// query GetUser($id: ID!) {
// user(id: $id) {
// id
// email
// name
// role
// posts(limit: 5) {
// id
// title
// status
// }
// }
// }
// Generated hook — fully typed
import { useGetUserQuery } from "../generated/operations";
function UserProfile({ userId }: { userId: string }) {
const { data, loading, error } = useGetUserQuery({
variables: { id: userId }, // Type-checked
});
if (loading) return <Spinner />;
if (error) return <Error message={error.message} />;
if (!data?.user) return <NotFound />;
const { user } = data;
return (
<div>
<h2>{user.name}</h2>
<p>{user.email}</p>
<span>{user.role}</span> {/* Typed as UserRole enum */}
<ul>
{user.posts.map((post) => (
<li key={post.id}>
{post.title} — {post.status}
</li>
))}
</ul>
</div>
);
}Gestión de la evolución del esquema
Los tipos cambian a medida que el producto evoluciona. La generación de código convierte los cambios del esquema en errores del compilador, haciendo visible de inmediato cada cambio disruptivo.
interface SchemaChange {
type: "field-added" | "field-removed" | "type-changed" | "field-deprecated";
path: string;
breaking: boolean;
}
function analyzeSchemaChanges(
oldSchema: string,
newSchema: string
): SchemaChange[] {
const changes: SchemaChange[] = [];
// Adding a nullable field — non-breaking
// Adding a required field — breaking for mutations
// Removing a field — breaking for queries using it
// Changing a field type — breaking
// After codegen runs, TypeScript surfaces the impact:
// - Removed field? Every resolver and query referencing it errors
// - Type changed? Every consumer with wrong type errors
// - New required input? Every mutation call missing it errors
return changes;
}
// CI pipeline: regenerate types on schema changes
// 1. Developer modifies schema.graphql
// 2. CI runs graphql-codegen
// 3. TypeScript compilation checks all resolvers and client queries
// 4. Build fails if any consumer doesn't match the new schema
// 5. Developer fixes consumers before mergingConclusiones clave
La generación de código GraphQL elimina la brecha de tipos entre el esquema, los resolvers y las consultas del cliente. El esquema se convierte en la única fuente de verdad, y cada tipo de TypeScript se deriva de él automáticamente. Los tipos de retorno de los resolvers se aplican de forma obligatoria: los campos faltantes o los tipos incorrectos son errores del compilador, no errores en tiempo de ejecución.
Ejecuta la generación de código en CI para que cada cambio en el esquema produzca tipos actualizados. Cuando se elimina un campo o cambia su tipo, el compilador de TypeScript identifica inmediatamente cada resolver y consulta del cliente afectados. Esto convierte la evolución del esquema en una verificación automatizada en lugar de una auditoría manual.
La inversión es un archivo de configuración de codegen y un paso de build. El retorno es seguridad de tipos de extremo a extremo en cada capa del stack GraphQL, eliminando toda una clase de errores que de otro modo solo aparecerían en producción.


