DEV Community

Cover image for Building a Catalog GraphQL API with InversifyJS - Part 2: Database Setup with Prisma & Docker
notaphplover
notaphplover

Posted on

Building a Catalog GraphQL API with InversifyJS - Part 2: Database Setup with Prisma & Docker

In Part 1, we set up our project structure and GraphQL type generation. In this second part, we will focus on the persistence layer. We will set up a PostgreSQL database using Docker, configure Prisma as our ORM, and implement our first repositories.

Step 1: Setting up PostgreSQL with Docker

To keep our development environment clean and consistent, we will run our database in a Docker container. We also need to create a shadow database for Prisma migrations. We'll use an initialization script for this.

First, create a directory postgresql and a file init_shadow_db.sql inside it:

CREATE DATABASE catalog_db_shadow;
Enter fullscreen mode Exit fullscreen mode

Now, create a docker-compose.yml file in the root of your project:

services:
  db:
    image: postgres:16-alpine
    environment:
      - POSTGRES_USER=postgres_user
      - POSTGRES_PASSWORD=password
      - POSTGRES_DB=catalog_db
    ports:
      - '5432:5432'
    volumes:
      - db_data:/var/lib/postgresql/data
      - ./postgresql/init_shadow_db.sql:/docker-entrypoint-initdb.d/init_shadow_db.sql

volumes:
  db_data:
    driver: local
Enter fullscreen mode Exit fullscreen mode

Now, you can start the database by running:

docker compose up -d
Enter fullscreen mode Exit fullscreen mode

This will start a PostgreSQL instance listening on port 5432.

Step 2: Installing and Configuring Prisma

Prisma is a modern ORM that we will use to interact with our database.

First, install the dependencies:

pnpm add @prisma/client @prisma/adapter-pg
pnpm add -D prisma
Enter fullscreen mode Exit fullscreen mode

Initialize Prisma in your project:

pnpm exec prisma init
Enter fullscreen mode Exit fullscreen mode

This creates a prisma directory with a schema.prisma file and adds a .env file to your project.

We will use a prisma.config.mjs file to configure Prisma. Create this file in the root of your project:

import path from 'node:path';
import 'dotenv/config';
import { defineConfig, env } from 'prisma/config';

export default defineConfig({
  datasource: {
    url: env('DATABASE_CONNECTION_STRING'),
    shadowDatabaseUrl: env('SHADOW_DATABASE_CONNECTION_STRING'),
  },
  schema: path.join('prisma', 'schema.prisma'),
});
Enter fullscreen mode Exit fullscreen mode

Update your .env file to point to your local Docker database:

DATABASE_CONNECTION_STRING="postgresql://postgres_user:password@localhost:5432/catalog_db?schema=public"
SHADOW_DATABASE_CONNECTION_STRING="postgresql://postgres_user:password@localhost:5432/catalog_db_shadow?schema=public"
Enter fullscreen mode Exit fullscreen mode

Step 3: Defining the Data Model

Open prisma/schema.prisma and define your data models. We will create Category and Product models. Note that we are generating the client to a custom location ../generated to keep it within our project structure.

generator client {
  provider = "prisma-client-js"
  output   = "../generated"
}

datasource db {
  provider = "postgresql"
}

model Category {
  id        String    @id @default(uuid(7)) @db.Uuid
  name      String
  slug      String    @unique
  createdAt DateTime  @default(now())
  updatedAt DateTime  @default(now()) @updatedAt
  products  Product[]

  @@map("category")
}

model Product {
  id          String   @id @default(uuid(7)) @db.Uuid
  categoryId  String   @db.Uuid
  title       String
  description String
  currency    String
  price       Float
  createdAt   DateTime @default(now())
  updatedAt   DateTime @default(now()) @updatedAt
  category    Category @relation(fields: [categoryId], references: [id], onDelete: NoAction)

  @@map("product")
}
Enter fullscreen mode Exit fullscreen mode

Step 4: Running Migrations

Now that we have our schema defined, let's create a migration to apply these changes to the database.

pnpm exec prisma migrate dev --name init
Enter fullscreen mode Exit fullscreen mode

This command will:

  1. Generate a SQL migration file.
  2. Execute the SQL against your database.
  3. Generate the Prisma Client.

Step 5: Implementing Repositories

We will use the Repository Pattern to abstract our data access logic. This makes our code more testable and decoupled.

First, we need to make sure PrismaClient is available for injection. We will import it from our generated client.

Category Repository

Create src/category/repositories/CategoryRepository.ts:

import { inject, injectable } from 'inversify';
import { PrismaClient } from '../../../generated/index.js';

@injectable()
export class CategoryRepository {
  readonly #client: PrismaClient;

  constructor(
    @inject(PrismaClient) client: PrismaClient
  ) {
    this.#client = client;
  }

  public async count(): Promise<number> {
    return this.#client.category.count();
  }
}
Enter fullscreen mode Exit fullscreen mode

Product Repository

Create src/product/repositories/ProductRepository.ts:

import { inject, injectable } from 'inversify';
import { PrismaClient } from '../../../generated/index.js';

@injectable()
export class ProductRepository {
  readonly #client: PrismaClient;

  constructor(
    @inject(PrismaClient) client: PrismaClient
  ) {
    this.#client = client;
  }

  public async count(): Promise<number> {
    return this.#client.product.count();
  }
}
Enter fullscreen mode Exit fullscreen mode

Step 6: Dependency Injection Setup

Now that we have our repositories, we need to configure our InversifyJS container modules. We will create separate modules for our database connection and our domain features.

Prisma Module

First, let's create a module to manage the PrismaClient instance. This ensures we have a single instance of the database client throughout our application. We will use the PrismaPg adapter for PostgreSQL.

Create src/foundation/db/modules/PrismaModule.ts:

import { PrismaPg } from '@prisma/adapter-pg';
import { ContainerModule, type ContainerModuleLoadOptions } from 'inversify';
import { PrismaClient } from '../../../../generated/index.js';

export class PrismaModule extends ContainerModule {
  constructor() {
    super((options: ContainerModuleLoadOptions) => {
      options.bind(PrismaClient).toConstantValue(
        new PrismaClient({
          adapter: new PrismaPg({
            connectionString:
              'postgresql://postgres_user:password@localhost:5432/catalog_db?schema=public',
          }),
        }),
      );
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

Category Module

Next, we create a module for the Category feature. This module will bind the CategoryRepository.

Create src/category/modules/CategoryContainerModule.ts:

import { ContainerModule, type ContainerModuleLoadOptions } from 'inversify';
import { CategoryRepository } from '../repositories/CategoryRepository.js';

export class CategoryContainerModule extends ContainerModule {
  constructor() {
    super((options: ContainerModuleLoadOptions): void => {
      options.bind(CategoryRepository).toSelf().inSingletonScope();
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

Product Module

Similarly, we create a module for the Product feature.

Create src/product/modules/ProductContainerModule.ts:

import { ContainerModule, type ContainerModuleLoadOptions } from 'inversify';
import { ProductRepository } from '../repositories/ProductRepository.js';

export class ProductContainerModule extends ContainerModule {
  constructor() {
    super((options: ContainerModuleLoadOptions): void => {
      options.bind(ProductRepository).toSelf().inSingletonScope();
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

In this part, we have successfully:

  1. Set up a PostgreSQL database using Docker.
  2. Configured Prisma and defined our database schema.
  3. Implemented our repositories using InversifyJS for dependency injection.
  4. Created InversifyJS container modules to manage our dependencies.

In the next part, we will wire everything together in the main application container and start building our GraphQL resolvers.

Top comments (0)