· 3 min read
#030

Leaky Abstraction in NestJS - DrizzleORM Example

What is Leaky Abstraction?

Leaky Abstraction refers to when an abstraction layer fails to hide his complexity, and forcing you to understand what is implemented within the abstraction layer so you can use it correctly.

To clarify, leaky abstraction is impossible to avoid. But, we should try our best to avoid it so it does not create more leaky abstraction problem.

Example

Below is a good example of leaky abstraction example. In order for me th throw the ConflictException error instead of just Unexpected 500 Error, I would have to know error cause code 23505 that Postgres throw.

//book.service.ts
try {
  const [created] = await this.db
    .insert(book)
    .values({ ... })
    .returning();
  return created;
} catch (error) {
  if (typeof error.cause === 'object' &&
    error.cause !== null &&
    'code' in error.cause &&
    error.cause.code === '23505') {
    throw new ConflictException('A book with this name and author already exists');
  }
  throw error;
}

How to Improve?

The idea here is use Repository. If service is responsible for business logic, then repository is used to abstract away all the ORM query.

The book.repository.ts will be handling the ORM query and error code and decide what kind of error to throw based on the ORM error code.

While the book.service.ts is deciding om the business logic flow and which repository to call and use.

Let’s get into the example.

  1. Let’s create a reusable function to detect unique violation.
// src/database/pg-errors/index.ts
import { DrizzleQueryError } from "drizzle-orm";

export function isUniqueViolation(error: unknown): boolean {
  const isUniqueViolation =
    error instanceof DrizzleQueryError &&
    typeof error.cause === "object" &&
    error.cause !== null &&
    "code" in error.cause &&
    error.cause.code === "23505";

  return isUniqueViolation;
}

The primary purpose is able to reuse this function across different repository.

  1. Create a book.repository.ts. to abstract away all the ORM related code.
// book.repository.ts
import { Inject, Injectable } from "@nestjs/common";
import type { DrizzleDB } from "../database/drizzle.provider";
import { DRIZZLE_PROVIDER } from "../database/drizzle.provider";
import { book } from "../database/schema/book";
import { BookConflictError } from "./errors/book-conflict.error";
import { isUniqueViolation } from "src/database/pg-errors";

@Injectable()
export class BookRepository {
  constructor(@Inject(DRIZZLE_PROVIDER) private readonly db: DrizzleDB) {}

  async insert(values: typeof book.$inferInsert) {
    try {
      const [created] = await this.db.insert(book).values(values).returning();
      return created;
    } catch (error) {
      if (isUniqueViolation(error)) {
        throw new BookConflictError();
      }
      throw error;
    }
  }
}
  1. Finally, update your book.service.ts to call the BookRepository insert function.
// book.service.ts
import { Injectable, ConflictException } from "@nestjs/common";
import { CreateBookDto } from "./dto/create-book.dto";
import { UpdateBookDto } from "./dto/update-book.dto";
import { BookConflictError } from "./errors/book-conflict.error";
import { BookRepository } from "./book.repository";
import { BookStatus } from "src/database/schema";

@Injectable()
export class BookService {
  constructor(private readonly bookRepository: BookRepository) {}

  async create(createBookDto: CreateBookDto) {
    try {
      return await this.bookRepository.insert({
        name: createBookDto.name,
        author: createBookDto.author,
        status: createBookDto.status ?? BookStatus.AVAILABLE,
        updatedAt: new Date(),
      });
    } catch (error) {
      if (error instanceof BookConflictError) {
        throw new ConflictException(error.message);
      }
      throw error;
    }
  }
}

If you enjoyed this post, consider subscribing.
Get my next post delivered to you.

All articles