跪拜 Guibai
← All articles
Backend

NestJS Controller, Service, Module: The Three Buckets Every Route Hits

By Z思学 ·
Read original on juejin.cn ↗ Google Translate ↗ Alt translation

Backend frameworks that enforce a strict Controller-Service-Module split prevent the spaghetti that happens when routing, business logic, and wiring get tangled in a single file. For a developer coming from Express or Flask, this is the structural difference that makes a NestJS codebase navigable at scale.

Summary

A NestJS request flows through Controller, Service, and Module in a straight line. The Controller is a thin router—decorators like @Get, @Post, @Param, and @Body map URLs to handlers and extract parameters, but no business logic lives there. The Service layer does the actual work: querying Prisma, calling external APIs, and transforming data. A single Service method can be reused across multiple Controllers, so changing business rules never touches routing code.

Module is the wiring layer. Its three properties—imports, controllers, and providers—tell the DI container which classes exist and how they connect. Registering a Service in providers makes it injectable into any Controller or other Service within that module. The article walks through a full CRUD UserService backed by Prisma and Neon PostgreSQL, then adds a findByEmail endpoint as a hands-on exercise.

Frontend developers get a direct mapping: Controller equals router.js, Service equals utils/ or api/, and Module equals the index.js barrel export that assembles everything.

Takeaways
Controller decorators (@Get, @Post, @Patch, @Delete, @Param, @Query, @Body) handle routing and parameter extraction; no business logic belongs here.
Service classes carry all business logic and database calls; a single Service method can be reused by multiple Controllers.
Module registers Controllers and providers so NestJS's DI container can inject them; the imports array pulls in other modules.
Prisma's five core methods—findMany, findUnique, create, update, delete—map directly to SQL SELECT, INSERT, UPDATE, and DELETE.
Adding a new endpoint means adding a Service method for the logic and a Controller route to expose it, then testing via the browser.
Conclusions

The Controller-Service split is not just about cleanliness—it decouples HTTP surface area from business rules so that changing a route never forces a rewrite of database logic, and vice versa.

NestJS's Module system acts as an explicit dependency graph. Without it, DI is invisible; with it, every class's dependencies are declared in one place, making the project self-documenting.

Mapping NestJS layers to frontend concepts (router.js, utils/, index.js barrel) lowers the entry barrier for full-stack JavaScript developers who already think in component trees and route configs.

Concepts & terms
Controller (NestJS)
A class decorated with @Controller() that defines HTTP route handlers. It receives requests, parses parameters via decorators like @Param and @Body, calls a Service, and returns a response. It should contain no business logic.
Service (NestJS)
A class decorated with @Injectable() that encapsulates business logic, database operations, and external API calls. Services are registered in a Module's providers array and injected into Controllers or other Services via the constructor.
Module (NestJS)
A class decorated with @Module() that groups related Controllers and Services. Its three properties—imports, controllers, providers—declare what other modules are needed, which Controllers handle requests, and which classes are available for dependency injection.
Dependency Injection (DI) in NestJS
NestJS's built-in DI container automatically instantiates and provides classes listed in a Module's providers array to any constructor that requests them, removing the need for manual wiring or singletons.
Source: juejin.cn ↗ Google Translate ↗ Backup ↗