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;
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
Now, you can start the database by running:
docker compose up -d
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
Initialize Prisma in your project:
pnpm exec prisma init
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'),
});
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"
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")
}
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
This command will:
- Generate a SQL migration file.
- Execute the SQL against your database.
- 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();
}
}
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();
}
}
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',
}),
}),
);
});
}
}
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();
});
}
}
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();
});
}
}
Conclusion
In this part, we have successfully:
- Set up a PostgreSQL database using Docker.
- Configured Prisma and defined our database schema.
- Implemented our repositories using InversifyJS for dependency injection.
- 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)