Step 12 of 29
feature modules, controllers/services/providers, IoC container, testing with overridden providers
modules, providers, constructor injection — โครงสร้างเดียวกับ Spring ที่ทีม backend รู้จักดี
NestJS organizes a backend into modules — each a @Module() class grouping related controllers and providers. Dependencies (services, repositories) are injected through the constructor by the framework's IoC container, not created by hand.
Unstructured Express apps rot into a pile of routes importing each other's helpers. NestJS imposes the structure production apps converge on anyway: modules as boundaries, services as logic, injected dependencies instead of singletons. The same architecture you know from Spring (Angular lineage is deliberate) — controllers/services/repositories, constructor injection, lifecycle hooks — which makes it a natural backend for React frontends and a résumé that transfers.
// tasks/tasks.module.ts
import { Module } from '@nestjs/common'
import { TasksController } from './tasks.controller'
import { TasksService } from './tasks.service'
@Module({
controllers: [TasksController],
providers: [TasksService],
exports: [TasksService] // other modules may import it
})
export class TasksModule {}
// app.module.ts — the root that composes everything
@Module({
imports: [AuthModule, TasksModule, MongooseModule.forRoot(...)]
})
export class AppModule {}
// tasks/tasks.service.ts
import { Injectable } from '@nestjs/common'
@Injectable()
export class TasksService {
constructor(private readonly repo: TaskRepository) {}
listForUser(userId: string) {
return this.repo.listForUser(userId)
}
}
The @Injectable() decorator registers the class with the module's container. When Nest instantiates TasksController, it sees TasksService in the constructor, resolves (or creates) it, and passes it in. You never new TasksService().
| Without DI | With DI |
|---|---|
new TaskRepository() in every file | One instance, injected where declared |
| Swap implementation = edit N call sites | Swap the provider token, done |
| Tests need real dependencies | Override with a fake in Test.createTestingModule |
const moduleRef = await Test.createTestingModule({
controllers: [TasksController],
providers: [
TasksService,
{ provide: TaskRepository, useValue: fakeRepo }
]
}).compile()
The controller under test never knows the difference.
Loading diagram...
Group by domain (tasks/, users/, auth/), each owning its controller, service, DTOs, and schema. Cross-module use goes through imports + exports — explicit dependencies between modules, no import cycles.
AppModule with 15 controllers. The module graph is the architecture — use it.new TasksService(new Repo()) bypasses the container, duplicates state, and blocks test overrides.TasksModule's private providers from AuthModule couples boundaries. Export what is public; import the module.