How the Repository Pattern Helped Us Migrate from REST to GraphQL Without Breaking Everything

dev.to

The Challenge I Faced

In our recent project, we started with a traditional REST API backend. The application was growing rapidly, and we realized we needed to migrate to a more flexible GraphQL API to reduce over-fetching and under-fetching issues.

The problem?
Our components were tightly coupled with REST API calls. Changing to GraphQL meant touching every single component that fetched data.

Solution?
but we had implemented the Repository Pattern from the very beginning, we were able to switch to GraphQL seamlessly — without touching every single component. The repository layer acted as a clean abstraction, so we only had to change the data-fetching logic in one place, while all components continued working with the same interfaces.

What is the Repository Pattern?

The Repository Pattern is a design pattern that mediates between the business logic layer and the data source layer. It acts as an abstraction layer that encapsulates the logic required to access data sources, providing a clean, consistent API for data operations.

In simple terms, a repository is like a middleman that handles all the complicated data-fetching logic and presents a simple interface to the rest of your application.

The beauty of this pattern is that your React components don't care where the data comes from. They just ask the repository for data, and the repository figures out how to get it.

Why Use the Repository Pattern in React?

React applications have evolved from simple view libraries to complex, state-rich applications. Here's why the Repository Pattern has become invaluable:

  • Separation of Concerns
  • Improved Testability
  • Centralized Logic
  • Easy Data Source Migration

When to Use the Repository Pattern?

1.Multiple Data Sources
Your app uses APIs, localStorage, and perhaps a WebSocket connection.

2.Complex Data Logic
You have caching strategies, retry mechanisms, or data transformation that would clutter components.

3.Frequent Testing
If you write unit tests and want to avoid hitting real APIs during tests.

4.Planned Architecture Changes
When you anticipate changing your data layer (e.g., moving from REST to GraphQL, or from Firebase to a custom backend).

Example

// domain/entities/Product.ts
export interface Product {
  id: string;
  name: string;
  price: number;
  category: string;
}

// domain/repositories/IProductRepo.ts
export interface IProductRepo {
  getProducts(): Promise<Product[]>;
}
Enter fullscreen mode Exit fullscreen mode
// infra/repositories/ProductRepoRest.ts
import { IProductRepo, Product } from '../../domain/entities/Product';

export class ProductRepoRest implements IProductRepo {
  private baseUrl = 'https://api.example.com/v1/products';

  async getProducts(): Promise<Product[]> {
    const response = await fetch(this.baseUrl);
    const data = await response.json();
    return data.items.map((item: any) => ({
      id: item.id,
      name: item.name,
      price: item.price,
      category: item.category
    }));
  }

}
Enter fullscreen mode Exit fullscreen mode
// infra/repositories/ProductRepoGraphQL.ts
import { IProductRepo, Product } from '../../domain/entities/Product';

export class ProductRepoGraphQL implements IProductRepo {
  private endpoint = 'https://api.example.com/graphql';

  async getProducts(): Promise<Product[]> {
    const query = `
      query GetProducts {
        products {
          id
          name
          price
          category
        }
      }
    `;

    const response = await fetch(this.endpoint, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ query })
    });

    const result = await response.json();
    return result.data.products;
  }

}
Enter fullscreen mode Exit fullscreen mode
// di/registry.ts

export const repoRegistry = {
  rest: {
    product: new ProductRepoRest() as IProductRepo,
  },
  graphql: {
    product: new ProductRepoGraphQL() as IProductRepo,
  },
};

export type RepoRegistry = typeof repoRegistry;
Enter fullscreen mode Exit fullscreen mode
// di/useDI.ts
import { create } from 'zustand';
import { repoRegistry, RepoRegistry } from './registry';

interface ServiceState {
  repos: RepoRegistry;
}

export const useDI = create<ServiceState>(() => ({
  repos: repoRegistry,
}));
Enter fullscreen mode Exit fullscreen mode
// app/hooks/useGetProducts.ts
export function useGetProducts(repo: IProductRepo) {
 /*
   logic...
   e.g. react query
*/

  return { products, loading, error };
}
Enter fullscreen mode Exit fullscreen mode
// components/ProductList.tsx

interface ProductListProps {
  repo?: IProductRepo;
}

export function ProductList({ repo }: ProductListProps) {

  const { products, loading, error } = useGetProducts(repo);

  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;

  return (
    <ul>
      {products.map((product) => (
        <li key={product.id}>
          {product.name} - ${product.price}
        </li>
      ))}
    </ul>
  );
}
Enter fullscreen mode Exit fullscreen mode
// pages/ProductsPage.tsx

export function ProductsPage() {
 /*
    before:
    const productRepo = useDi(state => state.repos.rest.product)
    return <ProductList productRepo={productRepo} />;
*/

 //then
    const productRepo = useDi(state => state.repos.graphql.product)
    return <ProductList productRepo={productRepo} />
}
Enter fullscreen mode Exit fullscreen mode

Source: dev.to

arrow_back Back to News