Repository Pattern in JavaScript

The Repository Pattern is a structural pattern that abstracts and centralizes data access operations, providing a consistent and simplified interface for the application to interact with data sources.

In essence, the Repository Pattern acts as a mediator or a middleman between the application’s business logic and the data storage, shielding the rest of the code from the details of how data is fetched, stored, or manipulated. It encapsulates the data access logic, which can include querying databases, making API calls, or working with any other data source, into a set of defined methods and operations.

Characteristics and Benefits of the Repository Pattern

Repository Pattern characteristics and benefits
  1. Abstraction: It provides an abstract layer for data operations, allowing developers to work with data using high-level methods rather than dealing with low-level data access code.
  2. Centralization: Data access logic is centralized within the repository, making it easier to manage, maintain, and modify.
  3. Consistency: It enforces a consistent way of interacting with data across the application, ensuring that data-related operations follow the same patterns and conventions.
  4. Testability: The pattern facilitates unit testing because you can easily replace the actual data access logic with mock data for testing purposes.
  5. Flexibility: It enables the switching of data sources or storage mechanisms without affecting the rest of the application. For example, you can switch from a relational database to a NoSQL database or an external API with minimal code changes.

Repository Pattern Implementation in JavaScript

Note: For more context on the implementation, please watch the video above.

To implement the repository pattern in JavaScript with Sequelize we first need to create an interface that describes it Sequelize model. Let’s go ahead and create TravelAttributes and TourAttributes interfaces.

interface TravelAttributes {
  id: string;
  name: string;
  description: string;
  slug: string;
  is_public: boolean;
  number_of_days: number;
  tours: Array<TourAttributes>;
  created_at: Date;
  updated_at: Date;
}

interface TourAttributes {
  id: string;
  travel_id: string;
  name: string;
  starting_date: Date;
  ending_date: Date;
  price: number;
  created_at: Date;
  updated_at: Date;
}

Now we can pass those interfaces as generics to Travel and Tour models.

@Table({
  timestamps: true,
  tableName: "travels",
  modelName: "Travel",
})
class Travel extends Model<TravelAttributes> {
  @Column({
    primaryKey: true,
    type: DataType.UUID,
    defaultValue: DataType.UUIDV4,
  })
  declare id: string;
...
@Table({
  timestamps: true,
  tableName: "tours",
  modelName: "Tour",
})
class Tour extends Model<TourAttributes> {
  @Column({
    primaryKey: true,
    type: DataType.UUID,
    defaultValue: DataType.UUIDV4,
  })
  declare id: string;
...

Now we can use TravelAttributes and TourAttributes as generics in Travel and Tour resources.

import BaseResource from "./BaseResource";
import TourResource from "./TourResource";

class TravelResource extends BaseResource<TravelAttributes, TravelEntity>() {
  item() {
    const travelResource: TravelEntity = {
      id: this.instance.id,
      name: this.instance.name,
      description: this.instance.description || undefined,
      slug: this.instance.slug,
      number_of_days: this.instance.number_of_days,
      tours: TourResource.collection(this.instance.tours),
    };

    return travelResource;
  }
}

export default TravelResource;
import BaseResource from "./BaseResource";

class TourResource extends BaseResource<TourAttributes, TourEntity>() {
  item() {
    const tourResource: TourEntity = {
      id: this.instance.id,
      travel_id: this.instance.travel_id,
      name: this.instance.name,
      starting_date: this.instance.starting_date,
      ending_date: this.instance.ending_date,
      price: this.instance.price,
    };

    return tourResource;
  }
}

export default TourResource;

The next step will be to create actual Travel and Tour repositories. We are going to use inheritance so Travel, Tour, and other repositories that come later will extend from Base repository class and re-use code as much as possible

import { ModelStatic } from "sequelize";

export default abstract class BaseRepository<A> {
  modelClass: ModelStatic<any>;

  constructor(modelClass: ModelStatic<any>) {
    this.modelClass = modelClass;
  }

  getAll(options: Record<string, any> = {}): Promise<Array<A>> {
    if (!options.hasOwnProperty("order")) {
      options = {
        ...options,
        ...this.getDefaultOrderBy(),
      };
    }

    return this.modelClass.findAll(options);
  }

  getById(id: string, options: Record<string, any> = {}): Promise<A> {
    return this.modelClass.findByPk(id, options);
  }

  protected getDefaultOrderBy() {
    return {
      order: [["created_at", "DESC"]],
    };
  }
}
import Tour from "../database/models/Tour";
import Travel from "../database/models/Travel";
import BaseRepository from "./BaseRepository";

export default class TravelRepository extends BaseRepository<TravelAttributes> {
  constructor() {
    super(Travel);
  }

  getAll(options: Record<string, any> = {}) {
    const opts = {
      ...options,
      ...this.getIncludes(),
    };
    return super.getAll(opts);
  }

  getById(id: string, options: Record<string, any> = {}) {
    const opts = {
      ...options,
      ...this.getIncludes(),
    };
    return super.getById(id, opts);
  }

  private getIncludes() {
    return {
      include: Tour,
    };
  }
}
import Tour from "../database/models/Tour";
import BaseRepository from "./BaseRepository";

export default class TourRepository extends BaseRepository<TourAttributes> {
  constructor() {
    super(Tour);
  }
}

As you can see, Travel repository has a more custom code, so it overwrites methods of the Base repository. However, Tour repository is quite simple and it uses methods of the parent class without implementing its own.

Finally, lets use the repository inside travel and tour controllers instead of direct calls to Travel and Tour Sequelize models.

const repository = new TravelRepository();
const travels = TravelResource.collection(await repository.getAll());

...

const repository = new TravelRepository();
const travelResource = new TravelResource(await repository.getById(req.params.id));
const repository = new TourRepository();
const tours = TourResource.collection(await repository.getAll());

...

const repository = new TourRepository();
const tourResource = new TourResource(await repository.getById(req.params.id));

Conlcusion

We hope you now have a better understanding of the Repository Pattern and how to implement it in JavaScript to abstract calls made to the database. As you continue your journey as a JavaScript developer, keep the Repository Pattern in your toolkit. Whether you’re building a small-scale application or a large-scale enterprise system, this pattern can help you maintain clean, organized, and maintainable code.

Share this article

Posted

in

by