Repository pattern in frontend
Repository pattern in frontend

Repository pattern in frontend

In the world of frontend development, managing data efficiently is crucial for building scalable and maintainable applications. One architectural pattern that has proven to be highly effective in achieving this goal is the Repository Pattern. Traditionally used in backend development, the Repository Pattern is increasingly being adopted in frontend development for its ability to decouple data access logic from business logic.

This has been growing with techniques like micro-frontend architecture where we need to decouple and isolate terms but also keep some elements shareables between projects to keep consistency here is where the data management can become a mess, and we need to take some architectural decisions to accomplish this consistency, and avoid rework.

The Repository Pattern is important for several reasons, that I call STAR-D

By incorporating the Repository Pattern, frontend developers can create applications that are not only easier to test and maintain but also more flexible and adaptable to changes.

Example

In this example, I will explore how to implement the Repository Pattern in a React application using TypeScript. I will create a folder structure that includes repositories for fetching data from an API and a mock data source. This setup allows us to switch bet en real and mock data sources seamlessly, demonstrating the flexibility and ease of testing that the Repository Pattern provides.

Folder Structure

In this folder structure, you can create a repository pattern that is framework-agnostic, making it suitable for use in any framework.

src/
  β”œβ”€β”€ repositories/
  β”‚   β”œβ”€β”€ clients/
  β”‚   β”‚   └── ApiClient.ts
  β”‚   β”œβ”€β”€ SummaryRepository.ts
  β”‚   β”œβ”€β”€ TransactionsRepository.ts
  β”‚   └── RepositoryFactory.ts
  β”œβ”€β”€ interfaces/
  β”‚   └── apis
  β”‚       └── summary
  β”‚           └── Get.d.ts
  β”‚           └── GetPaths.ts
  β”‚       └── transactions
  β”‚           └── Get.d.ts
  β”‚           └── GetPaths.ts
  β”‚           └── Post.d.ts
  β”‚   └── IApiClient.ts
  └── components/
      └── DataComponent.tsx

Implementation

Now, I will define each file in this folder structure, starting with the simplest one: the interface definitions. Here, we will define and agree upon the API or SDK contract between our component and the requested resource.

// src/interfaces/apis/summary/Get.d.ts
import { GetPaths } from "./GetPaths";

export interface ISummaryAccountsResponse {
  amount: number;
}
export interface ISummaryCardsResponse {
  amount: number;
}
type GetSummaryAccounts = (
  string: TPaths.SUMMARY_ACCOUNT,
) => Promise<ISummaryAccountsResponse>;
type GetSummaryCards = (
  string: TPaths.SUMMARY_CARD,
) => Promise<ISummaryCardsResponse>;
export type GetData = GetSummaryAccounts & GetSummaryCards;
export enum TPaths {
  SUMMARY_ACCOUNT = "summary",
  SUMMARY_CARD = "summary/card",
}

And the endpoint paths in GetPaths

// src/interfaces/apis/summary/GetPaths.ts
export enum GetPaths {
  KEY = "key",
  PULIC_KEY = "pubkey",
}

Transactions should be the same as before

// src/interfaces/apis/transactions/Get.ts
import { TPaths } from './GetPaths';

interface ITransaction{
    date: Date;
    description: string;
    amount: number;
    isCleared: boolean;
}
export interface ITransactions {
  transactions[]: ITransaction[];
}

type GetTransactions = (string:TPaths.TRANSACTIONS)=>Promise<ITransactions>;
export type GetData = GetTransactions;
// src/interfaces/apis/transactions/GetPaths.ts
export enum TPaths {
  TRANSACTIONS = "transactions",
}

All the elements defined in these interfaces are the ones that our already-made component requires to work properly.

Then lets go for more, lets work in the client and try to implement it in 2 ways an axios one and a fetch one

I can also use axios and simplify this client like this

// src/repositories/clients/apiClient.ts

import axios from "axios";

const BASE_URL = "https://api.example.com";
const TOKEN = "MY_JWT_TOKEN";
const ApiClient = axios.create(BASE_URL);
const addTokenInterceptor = (config) => {
  const newConfig = { ...config };
  return newConfig.commons.headers({
    Authorization: `Bearer ${TOKEN}`,
  });
};

ApiClient.interceptors.request.use(addTokenInterceptor);

export default ApiClient;

or use fetch like this

// IapiClient.d.ts
import { GetData as TGetData } from "./apis/transactions/Get";
import { GetData as SGetData } from "./apis/summary/Post";

type GetData = TGetData & SGetData;

export interface IApiClient {
  getData: GetData;
  postData: PostData;
}

This will allow us to use any method request in our repository file, and we can use these clients to fetch data in those repository files.

The repository file:

// src/repositories/summaryRepository.ts
// if you have request definitions import here like this
/*import {
  type ITransactionRequest,
} from './interfaces/apis/transactions/Get.ts';
*/
import ApiClient from "./clients/apiClient.ts";

const summaryRepository = {
  //and use the payload as param typed from ITransactionRequest
  async getSummary(): ISummary {
    const summary = await Apiclient.getData("summary")<ISummary>;
    /**
     * We can get the data, parse it to
     * comply with ISummary, and then return it.
     * */
    return summary;
  },
};

export default summaryRepository;

Now we can create as many repositories as we want.

Then we will use the repository factory to allow use one single factory method to get the repository

// src/repositories/RepositoryFactory.ts

import SummaryRepository, { ISummaryRepository } from "./summaryRepository.ts";
import OtherRepository, { IOtherRepository } from "./OtherRepository.ts";

type Repositories = "summary" | "other";
function get(string: "summary"): ISummaryRepository;
function get(string: "other"): IOtherRepository;
function get(string: Repositories): unknown;

function get(name: Repositories) {
  switch (name) {
    case "summary":
      return SummaryRepository;
    case "other":
      return OtherRepository;
    default:
      return null;
  }
}

export default {
  get,
};

This allow us to make test more easy than mocking axios with moxios or thigs like this because we can use a spy on the repository and mock the resolved value for it when we run the test

import SummaryRepository from '@/repository/summaryRepository.ts';

jest.spyOn(SummaryRepository,'getSummary').mockResolvedValue({
  amount: 100;
  currency: "$";
  accountNumber: "123-123-123";
});

As simple as this we can mock the method getSummary and get example data for our test or make it fail.

Please note that there might be errors in this code as it was not run; it is only meant to illustrate how to use the repository factory. If you find any issues, please report them to my email. β€œOnce I find the best solution, I will post it with the name of the person who fits best and was first to provide it ;) …

Link copied!

Comments for 000005