DEV Community

Cover image for Your NestJS Modules Don't Have to Be a Boilerplate Nightmare
Victor L. Bueno
Victor L. Bueno

Posted on

Your NestJS Modules Don't Have to Be a Boilerplate Nightmare

Ever opened a module file and immediately felt tired?

The Problem

Here's a typical module in a NestJS project I was working on:

import { Module } from '@nestjs/common';
import { LoggerService } from './logger/logger.service';
import { TokenService } from './token/token.service';
import { PaymentRepository } from './repositories/payment.repository';
import { OrderRepository } from './repositories/order.repository';
import { PaymentService } from './services/payment.service';
import { OrderService } from './services/order.service';

@Module({
  providers: [
    {
      provide: LoggerService,
      useFactory: () => new LoggerService('APP'),
    },
    {
      provide: TokenService,
      useFactory: () => new TokenService('my-secret-key'),
    },
    {
      provide: PaymentRepository,
      useClass: PaymentRepository,
    },
    {
      provide: OrderRepository,
      useClass: OrderRepository,
    },
    {
      provide: PaymentService,
      inject: [LoggerService, TokenService, PaymentRepository],
      useFactory: (logger: LoggerService, token: TokenService, repo: PaymentRepository) => {
        return new PaymentService(logger, token, repo);
      },
    },
    {
      provide: OrderService,
      inject: [LoggerService, OrderRepository, PaymentService],
      useFactory: (logger: LoggerService, orderRepo: OrderRepository, paymentService: PaymentService) => {
        return new OrderService(logger, orderRepo, paymentService);
      },
    },
  ],
  exports: [PaymentService, OrderService],
})
export class AppModule {}
Enter fullscreen mode Exit fullscreen mode

That's one module. Six providers. And I can barely tell what's going on without reading line by line.

Why This Sucks

A few things that drove me crazy:

It's repetitive. Every service follows the same pattern: provide, inject, useFactory. You're writing the same boilerplate over and over. Add a dependency? Update the inject array and the factory parameters. Miss one? Runtime error.

It's hard to read. Want to know what OrderService depends on? You have to find its provider block and parse the inject array. Now imagine 20 providers. Good luck.

It's scattered. Configuration, dependencies, instantiation—it's all mixed together in the module. There's no single place to see how your application is wired.

It doesn't scale. As the project grows, modules become these massive files. Shared infrastructure like databases and loggers get repeated across multiple modules. Your AppModule becomes a 100-line import fest.

Every time I added a new service, I found myself:

  1. Importing the class
  2. Adding it to providers
  3. Listing dependencies in inject
  4. Writing a factory
  5. Exporting if needed elsewhere
  6. Repeating in other modules that need it

I just wanted to write new SomeService(dependencyA, dependencyB) and move on with my life.

A Cleaner Approach With nestjs-moduly

nestjs-moduly takes a different approach. Instead of configuring providers in modules, you declare instances in one central place:

// instances.ts
import { createInstanceGroup } from 'nestjs-moduly';

export const Infrastructure = createInstanceGroup('Infrastructure');
export const Repository = createInstanceGroup('Repository');
export const Service = createInstanceGroup('Service');

// Singletons with configuration
Infrastructure.Logger = new LoggerService('APP');
Infrastructure.Token = new TokenService('my-secret-key');

// Repositories
Repository.Payment = new PaymentRepository();
Repository.Order = new OrderRepository();

// Services - dependencies are just constructor arguments
Service.Payment = new PaymentService(
  Infrastructure.Logger,
  Infrastructure.Token,
  Repository.Payment,
);

Service.Order = new OrderService(
  Infrastructure.Logger,
  Repository.Order,
  Service.Payment,
);
Enter fullscreen mode Exit fullscreen mode

Now your module is just:

// app.module.ts
import { Infrastructure, Repository, Service } from './instances';

@Module({
  imports: [
    Infrastructure.Logger,
    Infrastructure.Token,
    Repository.Payment,
    Repository.Order,
    Service.Payment,
    Service.Order,
  ],
  controllers: [OrderController, PaymentController],
})
export class AppModule {}
Enter fullscreen mode Exit fullscreen mode

No factories. No inject arrays. Just a list of what this module uses.

Why This Works Better

Less code, more clarity. You're writing actual TypeScript—new Service(dependency)—not configuration objects. The module's imports tell you exactly what it depends on.

Centralized instances. All your services, repositories, and infrastructure are declared in one place. Need to change the database config? One file. Need to see what OrderService depends on? Check instances.ts.

Easy refactoring. Move a service from one module to another? Just update the imports. No rewriting provider definitions.

Scales gracefully. As your app grows, instances.ts grows linearly. Your modules stay clean and readable.

Dependency graph is explicit. When you write Service.Order = new OrderService(Logger, Repository.Order, Service.Payment), the dependencies are obvious. No hunting through provider blocks.

How to Use It

First, install the package:

npm install nestjs-moduly
Enter fullscreen mode Exit fullscreen mode

Now let's set things up.

Step 1: Create instance groups

An instance group is a logical container for related instances. Think of it as a namespace—you might have one for databases, one for repositories, one for services, etc.

// src/instances.ts
import { createInstanceGroup } from 'nestjs-moduly';

export const Database = createInstanceGroup('Database');
export const Repository = createInstanceGroup('Repository');
export const Service = createInstanceGroup('Service');
Enter fullscreen mode Exit fullscreen mode

The string you pass ('Database', 'Repository', etc.) becomes the prefix for injection tokens. This is useful if you ever need to inject by token instead of class.

Step 2: Assign instances to groups

Now you assign your actual instances. Each assignment automatically wraps the instance as a NestJS module, so it can be imported anywhere:

import { DatabaseService } from './services/database.service';
import { UserRepository } from './repositories/user.repository';
import { UserService } from './services/user.service';

// Each assignment creates a registerable module
Database.Primary = new DatabaseService({ host: 'localhost', port: 5432 });
Repository.Users = new UserRepository(Database.Primary);
Service.User = new UserService(Repository.Users);
Enter fullscreen mode Exit fullscreen mode

Notice how UserRepository receives Database.Primary directly in its constructor? That's because these are real instances, not provider configurations. You're just writing normal TypeScript.

The singleton is created once here, and shared everywhere it's imported.

Step 3: Import in your modules

Use the instances in the imports array (not providers):

// src/app.module.ts
import { Module } from '@nestjs/common';
import { Database, Repository, Service } from './instances';
import { UserController } from './controllers/user.controller';

@Module({
  imports: [
    Database.Primary,
    Repository.Users,
    Service.User,
  ],
  controllers: [UserController],
})
export class AppModule {}
Enter fullscreen mode Exit fullscreen mode

Each instance you import makes that singleton available to the module's controllers and other providers.

Step 4: Inject normally

Your controllers and services don't change. Just inject by the class type:

// src/controllers/user.controller.ts
@Controller('users')
export class UserController {
  constructor(private readonly userService: UserService) {}

  @Get()
  findAll() {
    return this.userService.findAll();
  }
}
Enter fullscreen mode Exit fullscreen mode

No special decorators needed. The library registers each instance with its class as a token, so standard injection works out of the box.


That's it. No more useFactory, no more inject arrays, no more provider soup.

Check out the GitHub repo for more examples and documentation.


How do you handle module organization in larger NestJS projects? Always curious to hear different approaches.

Top comments (0)