DevPath · Learn to code ESPTEN

Data, ORMs and layered architecture

The Repository pattern

The coupling problem

If you scatter SQL queries (or ORM calls) all over the application, your business logic becomes coupled to the database. Switching from Postgres to another source, or writing tests without a real database, becomes very hard.

The solution: the repository

A repository concentrates all the data access of an entity behind a set of functions named after the domain, not after SQL:

The rest of the program calls these functions and does not know whether SQL, an ORM or an array sits underneath. That boundary is the valuable part.

function createUserRepo(db) {
  return {
    async list() {
      const { rows } = await db.query("SELECT * FROM users");
      return rows;
    },
    async findById(id) {
      const { rows } = await db.query(
        "SELECT * FROM users WHERE id = $1",
        [id]
      );
      return rows[0];
    },
    async create(user) {
      const { rows } = await db.query(
        "INSERT INTO users (email) VALUES ($1) RETURNING *",
        [user.email]
      );
      return rows[0];
    },
  };
}

In-memory repositories

Since the repository is just an interface (an object with functions), you can implement one in memory over an array for tests. The logic that uses it does not notice the difference: in production it receives the real repo; in tests, the in-memory one. That is what you will practice in the exercises.

Repository functions are usually async (they return a Promise), even if internally they are synchronous, so the signature does not change when moving from an array to a real database.

Put this into practice

DevPath is a hands-on course: you read the theory here; in the app you put it into practice with exercises that really run, offline.

Start free in the app →
← Data access: drivers and ORMsLayered architecture →