Build a GraphQL React App with TypeScript

Create type-safe data layers with GraphQL and Apollo Client. A comprehensive guide to building maintainable React applications with full TypeScript integration.

Building modern React applications requires efficient data fetching strategies, and GraphQL has emerged as a powerful solution for managing API interactions. When combined with TypeScript, developers gain type safety throughout the entire data layer, reducing runtime errors and improving developer productivity. This guide explores how to build robust GraphQL-powered React applications using TypeScript, covering everything from initial setup to advanced type generation patterns.

The synergy between GraphQL, React, and TypeScript creates a development experience where your frontend code knows exactly what data shapes to expect, your IDE provides intelligent autocomplete, and your build process catches type mismatches before they reach production. For teams building complex Node.js web APIs, integrating a type-safe GraphQL layer provides consistency across your entire stack. Whether you're building a new application or migrating from REST, our web development services team can help you implement these patterns effectively.

Why GraphQL with React and TypeScript

GraphQL fundamentally changes how frontend applications interact with APIs. Unlike REST, where endpoints return fixed data structures, GraphQL allows clients to specify exactly what data they need. This flexibility eliminates over-fetching and under-fetching problems that plague REST-based applications, particularly on mobile networks where bandwidth matters (LogRocket's GraphQL benefits analysis).

TypeScript adds a compile-time safety layer that catches errors before they reach users. When combined with GraphQL's schema-first approach, you get end-to-end type safety from your backend schema to your React components. Apollo Client 4.0 enforces required variables at the type level--TypeScript simply won't let you forget to provide required query parameters (Apollo GraphQL TypeScript documentation).

Key Benefits

  • Autocomplete across your data layer -- available fields appear as you type GraphQL queries
  • Safer refactoring -- TypeScript flags any code that depends on changed data shapes
  • Faster onboarding -- new team members explore your data layer through type definitions
  • Living documentation -- types in your code reduce gaps between implementation and understanding

Our approach to API development leverages these benefits to create maintainable, type-safe integrations. Combined with proper error handling patterns in React, your applications become resilient and maintainable.

Setting Up Apollo Client with TypeScript

Apollo Client remains the most popular choice for managing GraphQL data in React applications. Setting it up with TypeScript requires careful configuration to ensure full type safety throughout your application. The client needs to understand your GraphQL schema and provide typed responses to your components (Apollo GraphQL TypeScript documentation).

Creating an Apollo Client instance with TypeScript involves configuring the cache, link chain, and type policies. The InMemoryCache requires type policies when your schema uses interfaces or unions, helping Apollo understand how to cache and fragment-match results. For simpler schemas, the default cache configuration works well out of the box. Understanding memory leak patterns in Node.js applications helps you build more resilient backends that integrate cleanly with your GraphQL layer.

Apollo Client Configuration with TypeScript
1import { ApolloClient, InMemoryCache, HttpLink, from } from '@apollo/client';2import { onError } from '@apollo/client/link/error';3 4const errorLink = onError(({ graphQLErrors, networkError }) => {5 if (graphQLErrors) {6 graphQLErrors.forEach(({ message, locations, path }) =>7 console.log(8 `[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`9 )10 );11 }12 if (networkError) {13 console.log(`[Network error]: ${networkError}`);14 }15});16 17const client = new ApolloClient({18 link: from([errorLink, new HttpLink({ uri: 'https://api.example.com/graphql' })]),19 cache: new InMemoryCache(),20 defaultOptions: {21 watchQuery: {22 fetchPolicy: 'cache-and-network',23 },24 },25});

Configuring Apollo Provider

Wrapping your React application with ApolloProvider requires passing the typed client instance. The provider accepts a client prop and makes it available to any component in your tree through React context. For TypeScript, you don't need special typing--the generic ApolloClient class handles type inference automatically when you pass your configured client instance.

Apollo Provider Setup
1import { ApolloProvider } from '@apollo/client';2import { client } from './apollo-client';3 4function App() {5 return (6 <ApolloProvider client={client}>7 <YourMainContent />8 </ApolloProvider>9 );10}

Writing Type-Safe GraphQL Queries

Query construction in Apollo Client with TypeScript leverages the gql tag and TypedDocumentNode. The key to type safety is using TypedDocumentNode, which Apollo Client 4.0 promotes as the preferred way to associate queries with their result types (Apollo GraphQL TypeScript documentation).

When you use TypedDocumentNode, Apollo analyzes your query and generates TypeScript types for both the query variables and the response shape. The compiler can then validate that your components access only fields that exist in the response, catching typos and missing data access patterns at compile time rather than runtime.

TypedDocumentNode Definition
1import { gql } from '@apollo/client';2import { TypedDocumentNode } from '@apollo/client';3 4interface QueryVariables {5 id: string;6}7 8interface QueryResult {9 user: {10 id: string;11 name: string;12 email: string;13 posts: {14 id: string;15 title: string;16 }[];17 };18}19 20const GET_USER: TypedDocumentNode<QueryResult, QueryVariables> = gql`21 query GetUser($id: ID!) {22 user(id: $id) {23 id24 name25 email26 posts {27 id28 title29 }30 }31 }32`;

Using the Typed Query Hook

The useQuery hook automatically infers types from your TypedDocumentNode. You don't need to manually specify generic parameters--TypeScript reads the types from the typed document node and applies them to the hook's return value. This inference extends to loading, error, and data properties, providing complete type safety without boilerplate.

Apollo Client 4.0's type system now enforces required variables at compile time. If your query requires a variable and you don't provide it, TypeScript will error during compilation. This catches missing parameters before your code ever runs, eliminating an entire class of runtime errors.

Using Typed Query Hook
1import { useQuery } from '@apollo/client';2import { GET_USER } from './queries';3 4function UserProfile({ userId }: { userId: string }) {5 const { data, loading, error } = useQuery(GET_USER, {6 variables: { id: userId },7 });8 9 if (loading) return <LoadingSpinner />;10 if (error) return <ErrorMessage error={error} />;11 12 // data is fully typed - TypeScript knows the shape13 return (14 <div>15 <h1>{data.user.name}</h1>16 <p>{data.user.email}</p>17 <ul>18 {data.user.posts.map(post => (19 <li key={post.id}>{post.title}</li>20 ))}21 </ul>22 </div>23 );24}

Automatic Type Generation with GraphQL Code Generator

While manual TypedDocumentNode creation works, scaling to larger applications benefits from automatic type generation. GraphQL Code Generator reads your GraphQL operations and schema, then outputs TypeScript types that match your actual queries (GraphQL Code Generator React/Vue Guide). This approach ensures your types stay synchronized with your queries--any change to a query automatically regenerates corresponding types.

The GraphQL Code Generator workflow involves several steps: define your GraphQL operations in source files, scan files to extract queries and mutations, fetch your schema, and generate TypeScript files containing fully typed hook functions and result types. The generated files include TypeScript interfaces for all query results, variables, and fragments that you can import directly.

GraphQL Code Generator Configuration
1import type { CodegenConfig } from '@graphql-codegen/cli';2 3const config: CodegenConfig = {4 schema: 'https://api.example.com/graphql',5 documents: ['src/**/*.{ts,tsx}'],6 ignoreNoDocuments: true,7 generates: {8 './src/gql/': {9 preset: 'client',10 plugins: [],11 presetConfig: {12 gqlTagName: 'gql',13 },14 },15 },16};17 18export default config;

Setting Up the Development Workflow

Running GraphQL Code Generator in watch mode provides the best developer experience. As you modify queries, types regenerate automatically, and your IDE reflects the changes immediately. Add a script to your package.json to run the generator in watch mode, which monitors your source files for changes and regenerates types whenever queries change.

NPM Scripts for Code Generation
1{2 "scripts": {3 "generate": "graphql-codegen --watch",4 "generate:ci": "graphql-codegen"5 }6}

Performance Patterns and Best Practices

Building performant GraphQL applications requires attention to caching, data fetching strategies, and bundle size management. Apollo Client's normalized cache stores query results by their identifiers, enabling efficient updates when individual objects change. Understanding how to leverage this cache prevents unnecessary network requests and provides snappy user experiences.

Cache Strategies

The fetchPolicy option on useQuery controls how Apollo handles cached data:

  • cache-first -- Checks the cache before making network requests, ideal for stable data
  • network-only -- Always fetches fresh data, bypassing the cache
  • cache-and-network -- Returns cached data immediately while also fetching updates

Optimistic updates provide perceived performance improvements by immediately updating the UI before the server responds. If the mutation fails, Apollo rolls back the cache to its previous state and shows an error.

Cache Strategies and Optimistic Updates
1// Cache-first for stable data2const { data } = useQuery(GET_USER_PROFILE, {3 fetchPolicy: 'cache-first',4});5 6// Always fresh for real-time data7const { data } = useQuery(GET_LIVE_SCORES, {8 fetchPolicy: 'network-only',9});10 11// Optimistic updates for mutations12const [launchRocket] = useLaunchRocketMutation({13 optimisticResponse: {14 launchRocket: {15 id: '123',16 success: true,17 __typename: 'Launch',18 },19 },20 update(cache, { data: { launchRocket } }) {21 cache.modify({22 fields: {23 launches(existingLaunches = []) {24 const newLaunch = cache.writeFragment({25 data: launchRocket,26 fragment: LAUNCH_FRAGMENT,27 });28 return [...existingLaunches, newLaunch];29 },30 },31 });32 },33});

Pagination and Infinite Scrolling

Implementing pagination with Apollo Client leverages the fetchMore function returned by useQuery. This function allows you to execute the same query with different variables, merging results into the existing cache. The updateQuery function receives the existing cache contents and the new results, allowing you to define exactly how to merge them. For cursor-based pagination, you append new edges to the existing array and update the page info.

Bundle Size Considerations

Apollo Client 4.0 introduced modularization options that allow you to import only the features you need. For applications where bundle size is critical, you can tree-shake unused features or use the @apollo/client/core package for server-side rendering scenarios. GraphQL Code Generator's client preset generates minimal code focused on hooks and types, avoiding redundant type definitions.

Pagination with Apollo Client
1const FEED_QUERY = gql`2 query GetFeed($cursor: String) {3 feed(cursor: $cursor) {4 edges {5 node {6 id7 title8 content9 }10 }11 pageInfo {12 hasNextPage13 endCursor14 }15 }16 }17`;18 19function Feed() {20 const { data, fetchMore, hasMore } = useQuery(FEED_QUERY, {21 variables: { cursor: null },22 });23 24 const handleLoadMore = () => {25 if (!hasMore) return;26 27 fetchMore({28 variables: {29 cursor: data.feed.pageInfo.endCursor,30 },31 updateQuery(existing, { fetchMoreResult }) {32 if (!fetchMoreResult) return existing;33 return {34 feed: {35 __typename: 'FeedConnection',36 edges: [...existing.feed.edges, ...fetchMoreResult.feed.edges],37 pageInfo: fetchMoreResult.feed.pageInfo,38 },39 };40 },41 });42 };43 44 return (45 <div>46 {data.feed.edges.map(({ node }) => (47 <FeedItem key={node.id} item={node} />48 ))}49 {hasMore && <LoadMoreButton onClick={handleLoadMore} />}50 </div>51 );52}

Common Patterns and Anti-Patterns

Building maintainable GraphQL applications requires following consistent patterns while avoiding common pitfalls that create maintenance challenges as applications grow.

Effective Patterns

Co-locate queries with components -- Keep GraphQL operations near the components that use them. This makes it clear which data each component needs and simplifies refactoring. The GraphQL Code Generator's file-based generation supports this pattern by outputting types in the same locations as your source files (GraphQL Code Generator documentation).

Use fragments for shared fields -- When multiple components need the same data shape, define a fragment and spread it in queries. This ensures consistent field selection and makes schema changes affect all consumers simultaneously.

Handle errors at the component level -- GraphQL errors include messages and locations that help users understand what went wrong. Design error boundaries and fallback UI that communicate problems without crashing the entire application. Implementing proper React error boundaries provides a robust foundation for handling GraphQL errors gracefully.

Anti-Patterns to Avoid

Over-fetching data -- Requesting more fields than components need fills caches with unused data and increases response sizes. Analyze actual data requirements and request only what's displayed.

Ignoring loading states -- Failing to handle loading states creates poor user experiences with blank screens during data fetching. Always provide loading indicators that match your application's visual language.

Mutation-only error handling -- Focusing only on query errors while ignoring mutation errors leads to incomplete error handling. Mutations often represent critical user actions that require clear feedback when they fail.

For teams implementing these patterns, our frontend development services can provide guidance on building maintainable React applications.

Advanced TypeScript Patterns

TypeScript's advanced features enable sophisticated type safety patterns for complex GraphQL schemas. Conditional types, mapped types, and template literal types can transform GraphQL schema types into domain-specific TypeScript types that precisely match your application's needs.

For schemas with unions and interfaces, you can create discriminated union types that enable exhaustive type checking. The exhaustiveCheck function uses a type that requires all union members to be covered--if you add a new union variant without adding a case, TypeScript would error, ensuring your code always handles all possible types.

For applications with many similar queries, create base query hooks that handle common concerns like authentication, error handling, and loading states. These base hooks accept typed query documents and provide consistent behavior across your application, reducing boilerplate while maintaining full type safety.

Advanced TypeScript Patterns
1type User = IndividualUser | OrganizationUser;2 3function UserProfile({ user }: { user: User }) {4 if (user.__typename === 'IndividualUser') {5 return <IndividualProfile firstName={user.firstName} />;6 } else if (user.__typename === 'OrganizationUser') {7 return <OrganizationProfile orgName={user.orgName} />;8 }9 exhaustiveCheck(user);10}11 12// For applications with many similar queries, create base query hooks13function createTypedQueryHook<TData, TVariables>(14 query: TypedDocumentNode<TData, TVariables>15) {16 return function useTypedQuery(17 variables: TVariables,18 options?: Omit<UseQueryOptions<TData, TVariables>, 'query' | 'variables'>19 ) {20 return useQuery(query, { variables, ...options });21 };22}23 24export const useUserProfile = createTypedQueryHook(GET_USER_PROFILE);25export const useProductDetails = createTypedQueryHook(GET_PRODUCT);

Conclusion

Building GraphQL-powered React applications with TypeScript provides a development experience where type safety, developer productivity, and application performance align. Apollo Client's TypeScript support, combined with GraphQL Code Generator's automatic type creation, eliminates the traditional trade-off between type safety and development velocity. Your queries become self-documenting through their types, your IDE provides intelligent autocomplete, and your build process catches errors before they reach users.

The patterns covered in this guide--proper Apollo Client configuration, TypedDocumentNode usage, automatic type generation, caching strategies, and error handling--form a foundation for maintainable GraphQL applications. As your application grows, these patterns scale naturally, with type safety ensuring that changes to your schema propagate correctly through your entire codebase. Whether you're building a new application or adding GraphQL to an existing React project, TypeScript integration ensures your data layer remains reliable and maintainable.

Ready to modernize your web applications with type-safe GraphQL and React? Our team specializes in building performant, maintainable applications using modern technologies. Contact us to discuss how we can help your project.

Sources

  1. LogRocket: Build a GraphQL + React app with TypeScript - Comprehensive tutorial using SpaceX GraphQL API, covering Apollo Client setup, typed queries, and component integration patterns
  2. Apollo GraphQL: TypeScript with Apollo Client - Official documentation covering TypeScript integration in Apollo Client 4.0, including TypedDocumentNode and type inference
  3. The Guild: GraphQL Code Generator React/Vue Guide - Documentation on using GraphQL Code Generator to automatically generate TypeScript types from GraphQL operations

Ready to Modernize Your Web Applications?

Our team specializes in building type-safe, performant web applications using modern technologies like React, GraphQL, and TypeScript.