Menu
CoddyTech

NestJS Database Interview Questions

Connecting TypeORM, Prisma and Mongoose, transactions, migrations and N+1 queries.

10 questions
  1. How do you connect NestJS to a SQL database with TypeORM?

    Configure the connection once with TypeOrmModule.forRootAsync, register entities per feature with forFeature, and inject a repository with @InjectRepository.

    @Module({
      imports: [
        TypeOrmModule.forRootAsync({
          inject: [ConfigService],
          useFactory: (config: ConfigService) => ({
            type: 'postgres',
            url: config.get('DATABASE_URL'),
            autoLoadEntities: true,
            synchronize: false,
          }),
        }),
        UsersModule,
      ],
    })
    export class AppModule {}
    
    @Module({ imports: [TypeOrmModule.forFeature([User])], providers: [UsersService] })
    export class UsersModule {}
    
    @Injectable()
    export class UsersService {
      constructor(@InjectRepository(User) private users: Repository<User>) {}
      findByEmail(email: string) { return this.users.findOneBy({ email }); }
    }
    
  2. Why should synchronize: true never be used in production?

    Because TypeORM then alters the database schema at every startup to match your entities, with no review and no history. Renaming a property can drop the column and its data, and two instances starting at once can race on the same changes.

    Use migrations instead: generate a migration from entity changes, review the SQL, commit it, and run it as a deploy step.

    npx typeorm migration:generate src/migrations/AddOrderStatus -d src/data-source.ts
    npx typeorm migration:run -d src/data-source.ts
    

    synchronize: true is fine for a throwaway local database or a test run against an empty database.

  3. How do you use Prisma in a NestJS application?

    There is no official Nest module; the usual pattern is a PrismaService that extends the generated client, connects in a lifecycle hook, and is exported from a module.

    @Injectable()
    export class PrismaService extends PrismaClient implements OnModuleInit {
      async onModuleInit() {
        await this.$connect();
      }
    }
    
    @Global()
    @Module({ providers: [PrismaService], exports: [PrismaService] })
    export class PrismaModule {}
    
    @Injectable()
    export class OrdersService {
      constructor(private prisma: PrismaService) {}
      recent() {
        return this.prisma.order.findMany({ orderBy: { createdAt: 'desc' }, take: 20 });
      }
    }
    

    The schema lives in schema.prisma; prisma migrate dev creates migrations and prisma migrate deploy applies them in production. From Prisma 7, the client is generated into your project and needs a driver adapter, passed with super({ adapter }) in a constructor.

  4. How do you use MongoDB with Mongoose in NestJS?

    @nestjs/mongoose provides MongooseModule.forRoot() for the connection, decorators to define a schema from a class, and @InjectModel() for the model.

    @Schema({ timestamps: true })
    export class Product {
      @Prop({ required: true }) name: string;
      @Prop({ min: 0 }) price: number;
      @Prop({ index: true }) category: string;
    }
    export const ProductSchema = SchemaFactory.createForClass(Product);
    
    @Module({
      imports: [MongooseModule.forFeature([{ name: Product.name, schema: ProductSchema }])],
      providers: [ProductsService],
    })
    export class ProductsModule {}
    
    @Injectable()
    export class ProductsService {
      constructor(@InjectModel(Product.name) private products: Model<Product>) {}
      cheap() { return this.products.find({ price: { $lt: 10 } }).lean(); }
    }
    

    .lean() returns plain objects, which are faster to serialize when you do not need document methods. The MongoDB driver underneath works without Mongoose too.

  5. TypeORM or Prisma with NestJS: what are the differences?

    TypeORMPrisma
    Model defined inEntity classes with decoratorsschema.prisma file
    TypesFrom your classesGenerated from the schema
    QueriesRepository API, QueryBuilderTyped client, results typed per select
    Nest integrationOfficial @nestjs/typeormA small PrismaService you write
    MigrationsGenerated from entity diffprisma migrate from schema diff

    Prisma's result types follow each query's select, which catches more mistakes at compile time. TypeORM fits teams that want entities as classes. Either way, keep the ORM behind your services.

  6. How do you run several database writes in one transaction in NestJS?

    Use the ORM's transaction API and make every write inside it use the transactional handle, not the injected repository.

    // TypeORM
    async placeOrder(dto: PlaceOrderDto) {
      return this.dataSource.transaction(async (manager) => {
        const order = await manager.save(Order, { userId: dto.userId });
        await manager.decrement(Stock, { productId: dto.productId }, 'quantity', 1);
        return order;
      });
    }
    
    // Prisma
    await this.prisma.$transaction(async (tx) => {
      await tx.order.create({ data: { userId } });
      await tx.stock.update({ where: { productId }, data: { quantity: { decrement: 1 } } });
    });
    

    The classic bug: calling this.ordersRepository.save() inside the callback, which runs on another connection outside the transaction. To pass a transaction through several services without threading the manager, teams use AsyncLocalStorage based libraries (such as @nestjs-cls/transactional).

  7. Should services call the ORM directly or go through a repository class?

    For a small app, a service using the ORM's repository directly is fine. Add your own repository layer when queries grow complex or are reused, or when you want business logic testable without the ORM.

    @Injectable()
    export class OrdersRepository {
      constructor(@InjectRepository(Order) private repo: Repository<Order>) {}
    
      findOpenForUser(userId: string) {
        return this.repo.find({
          where: { userId, status: In(['pending', 'paid']) },
          order: { createdAt: 'DESC' },
        });
      }
    }
    

    The service then talks in domain terms (findOpenForUser) and a unit test mocks one small class instead of the ORM's query API. Do not wrap every ORM method one to one: that is code without a purpose.

  8. What is the N+1 query problem, and how do you fix it in a NestJS API?

    N+1 happens when you load a list with one query and then run one more query per row to load a relation: 1 query for 50 orders, plus 50 for their customers.

    Fixes, from the most common:

    • Load the relation in the same query: TypeORM relations: { customer: true } or a leftJoinAndSelect, Prisma include: { customer: true }.
    • Batch the lookups: collect the ids, run one WHERE id IN (...), and map the results.
    • In GraphQL resolvers, use DataLoader per request, which batches every customer field resolved in one tick into one query.

    Find it by logging queries in development (logging: true in TypeORM) and watching the count per request, not the time.

  9. How do you keep database credentials out of the code in NestJS?

    Read them from environment variables through @nestjs/config, validate them at startup, and inject ConfigService into the async module factory.

    ConfigModule.forRoot({
      isGlobal: true,
      validate: (env) => EnvSchema.parse(env), // fail fast on a missing variable
    }),
    TypeOrmModule.forRootAsync({
      inject: [ConfigService],
      useFactory: (config: ConfigService) => ({
        type: 'postgres',
        url: config.getOrThrow<string>('DATABASE_URL'),
      }),
    }),
    

    Never commit .env files with real secrets; production values come from the platform's secret store. In NestJS 12, @nestjs/config validates with Standard Schema (Zod and similar), and Joi still works from Joi 18 on.

  10. How do you run database migrations safely when deploying a NestJS service?

    Run migrations as a separate deploy step before the new version takes traffic, not from onModuleInit in every instance, where several replicas would race.

    Write changes so the old and new code both work during the rollout (expand and contract):

    1. Add the new column as nullable, deploy code that writes both.
    2. Backfill the data.
    3. Deploy code that reads only the new column.
    4. Drop the old column in a later release.

    Renaming a column in one step breaks the instances still running the old code. Keep each migration small, reviewed as SQL, and test it on a copy of production data before it runs for real.

Coddy programming languages illustration

Learn to code with Coddy

Get started