Building Type-Safe APIs with GraphQL Code Generation in TypeScript
Introduction
One of the biggest challenges in full-stack development is maintaining type safety between your frontend and backend. GraphQL promises a solution with its schema-first approach, but without proper tooling, you still end up with type mismatches and runtime errors. This is where GraphQL Code Generation shines, automatically creating TypeScript types from your GraphQL schema to ensure end-to-end type safety.
Why GraphQL Code Generation Matters
Traditional REST APIs often suffer from documentation drift and type inconsistencies. With GraphQL Code Generation, your types are always in sync with your schema. This means:
- Zero manual type definitions
- Compile-time error catching
- Better IDE support with autocomplete
- Reduced runtime errors
- Automatic refactoring support
Setting Up GraphQL Code Generation
Let's start by installing the necessary dependencies for a Node.js backend with TypeScript:
npm install @graphql-codegen/cli @graphql-codegen/typescript @graphql-codegen/typescript-resolvers
npm install --save-dev @graphql-codegen/typescript-operationsCreate a codegen.yml file in your project root:
overwrite: true
schema: "src/schema.graphql"
generates:
src/generated/graphql.ts:
plugins:
- "typescript"
- "typescript-resolvers"
config:
useIndexSignature: true
mappers:
User: "../models/User#User"
Post: "../models/Post#Post"Creating Your GraphQL Schema
Define your schema in src/schema.graphql:
type User {
id: ID!
email: String!
name: String!
posts: [Post!]!
createdAt: DateTime!
}
type Post {
id: ID!
title: String!
content: String!
author: User!
published: Boolean!
createdAt: DateTime!
}
type Query {
users: [User!]!
user(id: ID!): User
posts: [Post!]!
post(id: ID!): Post
}
type Mutation {
createUser(input: CreateUserInput!): User!
createPost(input: CreatePostInput!): Post!
updatePost(id: ID!, input: UpdatePostInput!): Post!
}
input CreateUserInput {
email: String!
name: String!
}
input CreatePostInput {
title: String!
content: String!
authorId: ID!
}
input UpdatePostInput {
title: String
content: String
published: Boolean
}
scalar DateTimeImplementing Type-Safe Resolvers
After running npx graphql-codegen, you'll get generated types. Now implement your resolvers with full type safety:
import { Resolvers, User, Post } from './generated/graphql';
import { Context } from './context';
export const resolvers: Resolvers = {
Query: {
users: async (_, __, { dataSources }) => {
return await dataSources.userService.getAllUsers();
},
user: async (_, { id }, { dataSources }) => {
return await dataSources.userService.getUserById(id);
},
posts: async (_, __, { dataSources }) => {
return await dataSources.postService.getAllPosts();
}
},
Mutation: {
createUser: async (_, { input }, { dataSources }) => {
// TypeScript knows the exact shape of 'input'
return await dataSources.userService.createUser(input);
},
createPost: async (_, { input }, { dataSources }) => {
const post = await dataSources.postService.createPost(input);
// Return type is automatically validated
return post;
}
},
User: {
posts: async (parent, _, { dataSources }) => {
return await dataSources.postService.getPostsByUserId(parent.id);
}
},
Post: {
author: async (parent, _, { dataSources }) => {
return await dataSources.userService.getUserById(parent.authorId);
}
}
}; Frontend Integration with React
For the frontend, install additional packages:
npm install @graphql-codegen/typescript-react-apolloUpdate your codegen.yml to generate React hooks:
generates:
src/generated/graphql.tsx:
documents: "src/**/*.graphql"
plugins:
- "typescript"
- "typescript-operations"
- "typescript-react-apollo"
config:
withHooks: trueCreate query files like src/queries/users.graphql:
query GetUsers {
users {
id
name
email
createdAt
}
}
mutation CreateUser($input: CreateUserInput!) {
createUser(input: $input) {
id
name
email
}
}Use the generated hooks in your React components:
import React from 'react';
import { useGetUsersQuery, useCreateUserMutation } from '../generated/graphql';
const UsersList: React.FC = () => {
const { data, loading, error } = useGetUsersQuery();
const [createUser] = useCreateUserMutation();
const handleCreateUser = async () => {
try {
await createUser({
variables: {
input: {
name: "John Doe",
email: "john@example.com"
}
}
});
} catch (err) {
console.error('Error creating user:', err);
}
};
if (loading) return Loading...;
if (error) return Error: {error.message};
return (
{data?.users.map(user => (
{user.name}
{user.email}
))}
);
};Best Practices and Advanced Configuration
Consider these optimizations for production use:
- Custom Scalars: Define proper TypeScript types for custom scalars like DateTime
- Mappers: Map GraphQL types to your actual database models
- Validation: Combine with libraries like Yup or Joi for runtime validation
- Caching: Use generated types with Apollo Client's cache
- CI/CD Integration: Add type generation to your build pipeline
Conclusion
GraphQL Code Generation transforms your development workflow by eliminating type mismatches and providing incredible developer experience. By automatically generating types from your schema, you catch errors at compile-time rather than runtime, leading to more robust applications. The initial setup investment pays dividends in reduced bugs, better refactoring capabilities, and improved team productivity.
Related Posts
Building Real-Time Chat Applications with Socket.io and Node.js
Learn how to build scalable real-time chat applications using Socket.io and Node.js with authentication and room management.
Building Type-Safe APIs with GraphQL Code-First Approach in NestJS
Learn how to create robust, type-safe GraphQL APIs using NestJS's code-first approach with TypeScript decorators and automatic schema generation.
Implementing Clean Architecture with NestJS: Building Scalable Enterprise Applications
Learn how to implement Clean Architecture principles in NestJS applications for better maintainability and testability.