Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Nest.js with Drizzle ORM

Are there any more elegant ways to use Drizzle ORM in Nest.js besides the providers? For example like in Prisma with PrismaService, all I found is only with providers like:

import { Module } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';

import { drizzle } from 'drizzle-orm/postgres-js';
import postgres from 'postgres';
import * as schema from './schema';

export const PG_CONNECTION = "PG_CONNECTION"; // ignore that it is not separate file

@Module({
  providers: [
    {
      provide: PG_CONNECTION,
      inject: [ConfigService],
      useFactory: async (config: ConfigService) => {
        const connection = postgres(config.get("DATABASE_URL"));
        return drizzle(connection, { schema });
      },
    },
  ],
  exports: [PG_CONNECTION],
})
export class DrizzleModule {}

and then:

import { Inject, Injectable } from '@nestjs/common';
import { PG_CONNECTION } from '../drizzle/drizzle.module';
import { dbType } from 'drizzle-orm/postgres-js'; // ignore, it doesn't matter yet
import * as schema from '../drizzle/schema';

@Injectable()
export class UsersService {
  constructor(@Inject(PG_CONNECTION) private drizzle: dbType<typeof schema>) {} // what I'm talking about

  async findAll() {
    return await this.drizzle.query.users.findMany();
  }
}

what I'm talking about, we should inject (provider?) every time, import the constant and type from the db client, instead of this:

import { Inject, Injectable } from '@nestjs/common';
import { PrismaService } from 'src/prisma/prisma.service'

@Injectable()
export class UsersService {
  constructor(private prisma: PrismaService) {}

  async findAll() {
    return await this.prisma.users.findMany();
  }
}

I tried to find a solution but it seems this topic is really not popular.. I was expecting such a big Nest.js and Drizzle ORM community to have a good solution to use Drizzle with Nest

like image 370
kilian.sch Avatar asked Jun 20 '26 06:06

kilian.sch


2 Answers

You could create your own wrapper class around the PG_CONNECTION injection token. Something like

@Injectable()
export class DrizzleService {
  constructor(@Inject(PG_CONNECTION) readonly db: dbType<typeof schema>) {}
}

And now instead of exporting the PG_CONNECTION token from your DrizzleModule you export the DrizzleService. This would allow you to use the similar private readonly drizzle: DrizzleService as you're able to with prisma and then access this.drizzle.db for your typed database access inside of other providers.

like image 112
Jay McDoniel Avatar answered Jun 22 '26 19:06

Jay McDoniel


I had a slightly easier approach that didn't involve much. Here is an example of how i did it.

'database.service.ts':

import { Injectable, OnModuleInit } from '@nestjs/common';

import { neon } from '@neondatabase/serverless';
import { drizzle } from 'drizzle-orm/neon-http';
import { db, DatabaseType } from 'drizzle/db';

import {
  difficultyEnum,
  topicTagEnum,
  badgeEnum,
} from 'drizzle/schema/enums/enums';
import * as schema from '../drizzle/schema';

@Injectable()
export class DatabaseService implements OnModuleInit {
  private db: DatabaseType;

  constructor() {
    this.db = db;
  }

  async onModuleInit() {
    try {
      const sql = neon(process.env.DB_URL as string);
      this.db = drizzle(sql, {
        schema,
        logger: true,
      });

      console.log('Database connected successfully');
    } catch (error) {
      console.error('Failed to connect to the database', error);
      throw error;
    }
  }

  getDb() {
    return this.db;
  }
}

'database.module.ts':

import { Module } from '@nestjs/common';
import { DatabaseService } from './database.service';

@Module({
  providers: [DatabaseService],
  exports: [DatabaseService], // As we want to share an instance of the 'DatabaseService' between several other modules, we need to export the 'DatabaseService' provider.
})
export class DatabaseModule {}

'app.module.ts':

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { QuestionsModule } from './questions/questions.module';
import { DatabaseModule } from './database/database.module';

@Module({
  imports: [
    QuestionsModule,
    DatabaseModule,
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

'questions.module.ts':

import { Module } from '@nestjs/common';
import { QuestionsService } from './questions.service';
import { QuestionsController } from './questions.controller';
import { DatabaseModule } from 'src/database/database.module';

// This is a feature module
@Module({
  imports: [DatabaseModule], // This means 'QuestionsModule' can use any providers (service) that 'DatabaseModule' exports.
  controllers: [QuestionsController], // This is the controller that handles incoming HTTP requests related to questions (like fetching, adding, or deleting questions).
  providers: [QuestionsService], // This is included as a provider which is responsible for the business logic, such as interacting with the database to manage questions.
})
export class QuestionsModule {}

'questions.service.ts':

import { Injectable } from '@nestjs/common';
import { CreateQuestionDto } from './dto/create-question.dto';
import { UpdateQuestionDto } from './dto/update-question.dto';

import { eq } from 'drizzle-orm';
import { DatabaseService } from 'src/database/database.service';

import {
  questionTable,
  QuestionTableType,
  NewQuestionTableType,
} from '../../drizzle/schema/models/question.model';
import { TopicTagType } from 'drizzle/schema/enums/enums';

@Injectable()
export class QuestionsService {
  // This is a default constructor
  constructor(private readonly databaseService: DatabaseService) {}

  async create(createQuestionDto: CreateQuestionDto) {
    return 'This action adds a new question';
  }

  // GET all questions
  async findAllQuestions(): Promise<QuestionTableType[]> {
    console.log('question.services.ts findAllQuestions() called'); // debug

    try {
      const fetchedQuestions = await this.databaseService
        .getDb()
        .query.questionTable.findMany();

      console.log(
        `Database Fetch: Retrieved ${fetchedQuestions.length} questions.`,
        fetchedQuestions,
      ); // debug

      return fetchedQuestions as QuestionTableType[];
    } catch (error) {
      console.error('Error fetching questions from database:', error);
      throw new Error('Error fetching questions from database');
    }
  }

  async findQuestionById(
    questionId: number,
  ): Promise<QuestionTableType | null> {
    console.log('question.services.ts findQuestionById() called'); // debug

    try {
      const fetchedQuestion: QuestionTableType | undefined =
        await this.databaseService.getDb().query.questionTable.findFirst({
          where: eq(questionTable.id, questionId),
        });

      // Guard clause
      if (!fetchedQuestion) {
        console.error('Question not found in database');
        return null;
      }

      console.log('Database Fetch: Retrieved question:', fetchedQuestion); // debug

      return fetchedQuestion;
    } catch (error) {
      console.error('Error fetching question from database:', error);
      throw new Error('Error fetching question from database');
    }
  }

  async findQuestionsByTopic(
    topic: TopicTagType,
  ): Promise<QuestionTableType[]> {
    console.log('question.services.ts findQuestionsByTopic() called'); // debug

    try {
      const fetchedQuestions: QuestionTableType[] = await this.databaseService
        .getDb()
        .query.questionTable.findMany({
          where: eq(questionTable.topicTag, topic),
        });

      // Guard clause
      if (!fetchedQuestions) {
        console.error('Questions not found in database');
        return null;
      }

      console.log(
        `Database Fetch: Retrieved ${fetchedQuestions.length} questions by topic ${topic}`,
        fetchedQuestions,
      ); // debug

      return fetchedQuestions;
    } catch (error) {
      console.error('Error fetching questions from database:', error);
      throw new Error('Error fetching questions from database');
    }
  }

  async update(id: number, updateQuestionDto: UpdateQuestionDto) {
    return `This action updates a #${id} question`;
  }

  async remove(id: number) {
    return `This action removes a #${id} question`;
  }
}

'questions.controller.ts':

import {
  Controller,
  Get,
  Post,
  Patch,
  Delete,
  Body,
  Param,
  NotFoundException,
  ValidationPipe,
  ParseIntPipe,
  ParseFloatPipe,
  ParseBoolPipe,
  ParseArrayPipe,
  ParseUUIDPipe,
  ParseEnumPipe,
  DefaultValuePipe,
  ParseFilePipe,
} from '@nestjs/common';
import { QuestionsService } from './questions.service';
import { CreateQuestionDto } from './dto/create-question.dto';
import { UpdateQuestionDto } from './dto/update-question.dto';
import { topicTagEnum, TopicTagType } from '../../drizzle/schema/enums/enums';

import {
  QuestionTableType,
  NewQuestionTableType,
} from 'drizzle/schema/models/question.model';

@Controller('questions')
export class QuestionsController {
  constructor(private readonly questionsService: QuestionsService) {}

  // This is an example method decorator that defines a route handler for POST requests to the specified route.
  @Post()
  create(@Body() createQuestionDto: CreateQuestionDto) {
    return this.questionsService.create(createQuestionDto);
  }

  @Get()
  findAll(): Promise<QuestionTableType[]> {
    return this.questionsService.findAllQuestions();
  }

  @Get(':id')
  async findQuestionById(
    @Param('id', ParseIntPipe) id: number,
  ): Promise<QuestionTableType> {
    const question = await this.questionsService.findQuestionById(id);

    // Guard clause
    if (!question) {
      throw new NotFoundException(`Question with ID ${id} not found.`);
    }
    
    return question;
  }

  // @Get('topic/:topic')
  // findQuestionsByTopic(
  //   @Param('topic', new ParseEnumPipe(topicTagEnum)) topic: TopicTagType,
  // ): Promise<QuestionTableType[]> {
  //   const questions = this.questionsService.findQuestionsByTopic(topic);

  //   // Guard clause
  //   if (!questions) {
  //     throw new NotFoundException(`Questions with topic ${topic} not found.`);
  //   }

  //   return questions;
  // }

  @Get('topic/:topic')
  findQuestionsByTopic(
    @Param('topic') topic: TopicTagType,
  ): Promise<QuestionTableType[]> {
    const questions = this.questionsService.findQuestionsByTopic(topic);

    // Guard clause
    if (!questions) {
      throw new NotFoundException(`Questions with topic ${topic} not found.`);
    }

    return questions;
  }

  @Patch(':id')
  update(
    @Param('id') id: string,
    @Body() updateQuestionDto: UpdateQuestionDto,
  ) {
    return this.questionsService.update(+id, updateQuestionDto);
  }

  @Delete(':id')
  remove(@Param('id') id: string) {
    return this.questionsService.remove(+id);
  }
}
like image 26
Joel D'Souza Avatar answered Jun 22 '26 17:06

Joel D'Souza



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!