Skip to content

Repository Pattern

intermediate2 min read
  • Prisma
  • NestJS
  • Node.js

A pattern that puts all data access for an entity behind a dedicated interface, so business logic works with a collection-like abstraction instead of raw queries.

Prerequisites

  • Services and an ORM such as Prisma (see Related Topics)

What you'll understand

  • What the repository pattern is and what it hides
  • Why isolating data access from business logic helps
  • The tradeoff between abstraction and simplicity

Explanation

The repository pattern puts all the data access for a given entity behind a single, dedicated object, the repository. Instead of scattering queries throughout your services, the rest of the application asks the repository for what it needs, findById, save, delete, and the repository is the only place that knows how those operations are actually performed against the database.

The benefit is separation of concerns. Business logic in a service (see Related Topics) works against a clean, collection-like interface and stays free of query details. Because the data access sits behind that interface, you can change how persistence works, swap a raw query for an ORM call, or substitute a fake repository in a test, without touching the logic that uses it. This makes services easier to read and to test in isolation.

It is worth being honest about the tradeoff. A modern ORM like Prisma is already a data-access abstraction, so wrapping every model in a hand-written repository can be redundant for a small application. The pattern earns its keep when data access is complex, when you want to isolate the rest of the code from a specific ORM, or when testability demands a swappable seam. Reach for it deliberately, not reflexively.

Examples

This TypeScript example defines a repository interface and a Prisma-backed implementation, so callers depend on the interface, not on Prisma:

interface UserRepository {
  findById(id: number): Promise<User | null>;
  create(data: CreateUser): Promise<User>;
}

class PrismaUserRepository implements UserRepository {
  constructor(private readonly prisma: PrismaClient) {}

  findById(id: number) {
    return this.prisma.user.findUnique({ where: { id } });
  }
  create(data: CreateUser) {
    return this.prisma.user.create({ data });
  }
}

A service depends on the UserRepository interface, so a test can pass a fake implementation and never touch a database (see Related Topics).

Common mistakes

  • Wrapping an already-capable ORM in thin repositories that add no value.
  • Letting query details leak through the interface, defeating the abstraction.
  • Putting business logic inside the repository instead of in the service.
  • Building one giant repository for many entities rather than one per entity.

Best practices

  • Keep each repository focused on a single entity and its persistence.
  • Expose a clean, intention-revealing interface and hide the query details.
  • Have services depend on the repository interface, not a concrete database client.
  • Adopt the pattern where it earns its cost, not for every trivial model.

Further reading

Related topics